pyqrack-cpu 1.82.0__py3-none-macosx_15_0_arm64.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.
@@ -0,0 +1,1518 @@
1
+ # (C) Daniel Strano and the Qrack contributors 2017-2025. All rights reserved.
2
+ #
3
+ # Use of this source code is governed by an MIT-style license that can be
4
+ # found in the LICENSE file or at https://opensource.org/licenses/MIT.
5
+ import math
6
+ import os
7
+ import random
8
+ import sys
9
+ import time
10
+
11
+ from .qrack_simulator import QrackSimulator
12
+ from .pauli import Pauli
13
+
14
+
15
+ _IS_QISKIT_AVAILABLE = True
16
+ try:
17
+ from qiskit.circuit.quantumcircuit import QuantumCircuit
18
+ from qiskit.compiler import transpile
19
+ from qiskit.quantum_info.operators.symplectic.clifford import Clifford
20
+ except ImportError:
21
+ _IS_QISKIT_AVAILABLE = False
22
+
23
+ _IS_QISKIT_AER_AVAILABLE = True
24
+ try:
25
+ from qiskit_aer.noise import NoiseModel, depolarizing_error
26
+ except ImportError:
27
+ _IS_QISKIT_AER_AVAILABLE = False
28
+
29
+
30
+ # Initial stub and concept produced through conversation with Elara
31
+ # (the custom OpenAI GPT)
32
+ class LHVQubit:
33
+ def __init__(self, toClone=None):
34
+ # Initial state in "Bloch vector" terms, defaults to |0⟩
35
+ if toClone:
36
+ self.bloch = toClone.bloch.copy()
37
+ else:
38
+ self.reset()
39
+
40
+ def reset(self):
41
+ self.bloch = [0.0, 0.0, 1.0]
42
+
43
+ def h(self):
44
+ # Hadamard: rotate around Y-axis then X-axis (simplified for LHV)
45
+ x, y, z = self.bloch
46
+ self.bloch = [(x + z) / math.sqrt(2), y, (z - x) / math.sqrt(2)]
47
+
48
+ def x(self):
49
+ x, y, z = self.bloch
50
+ self.bloch = [x, y, -z]
51
+
52
+ def y(self):
53
+ x, y, z = self.bloch
54
+ self.bloch = [-x, y, z]
55
+
56
+ def z(self):
57
+ x, y, z = self.bloch
58
+ self.bloch = [x, -y, z]
59
+
60
+ def rx(self, theta):
61
+ # Rotate Bloch vector around X-axis by angle theta
62
+ x, y, z = self.bloch
63
+ cos_theta = math.cos(theta)
64
+ sin_theta = math.sin(theta)
65
+ new_y = cos_theta * y - sin_theta * z
66
+ new_z = sin_theta * y + cos_theta * z
67
+ self.bloch = [x, new_y, new_z]
68
+
69
+ def ry(self, theta):
70
+ # Rotate Bloch vector around Y-axis by angle theta
71
+ x, y, z = self.bloch
72
+ cos_theta = math.cos(theta)
73
+ sin_theta = math.sin(theta)
74
+ new_x = cos_theta * x + sin_theta * z
75
+ new_z = -sin_theta * x + cos_theta * z
76
+ self.bloch = [new_x, y, new_z]
77
+
78
+ def rz(self, theta):
79
+ # Rotate Bloch vector around Z-axis by angle theta (in radians)
80
+ x, y, z = self.bloch
81
+ cos_theta = math.cos(theta)
82
+ sin_theta = math.sin(theta)
83
+ new_x = cos_theta * x - sin_theta * y
84
+ new_y = sin_theta * x + cos_theta * y
85
+ self.bloch = [new_x, new_y, z]
86
+
87
+ def s(self):
88
+ self.rz(math.pi / 2)
89
+
90
+ def adjs(self):
91
+ self.rz(-math.pi / 2)
92
+
93
+ def t(self):
94
+ self.rz(math.pi / 4)
95
+
96
+ def adjt(self):
97
+ self.rz(-math.pi / 4)
98
+
99
+ def u(self, theta, phi, lam):
100
+ # Apply general single-qubit unitary gate
101
+ self.rz(lam)
102
+ self.ry(theta)
103
+ self.rz(phi)
104
+
105
+ # Provided verbatim by Elara (the custom OpenAI GPT):
106
+ def mtrx(self, matrix):
107
+ """
108
+ Apply a 2x2 unitary matrix to the LHV Bloch vector using only standard math/cmath.
109
+ Matrix format: [a, b, c, d] for [[a, b], [c, d]]
110
+ """
111
+ a, b, c, d = matrix
112
+
113
+ # Current Bloch vector
114
+ x, y, z = self.bloch
115
+
116
+ # Convert to density matrix ρ = ½ (I + xσx + yσy + zσz)
117
+ rho = [[(1 + z) / 2, (x - 1j * y) / 2], [(x + 1j * y) / 2, (1 - z) / 2]]
118
+
119
+ # Compute U * ρ
120
+ u_rho = [
121
+ [a * rho[0][0] + b * rho[1][0], a * rho[0][1] + b * rho[1][1]],
122
+ [c * rho[0][0] + d * rho[1][0], c * rho[0][1] + d * rho[1][1]],
123
+ ]
124
+
125
+ # Compute (U * ρ) * U†
126
+ rho_prime = [
127
+ [
128
+ u_rho[0][0] * a.conjugate() + u_rho[0][1] * b.conjugate(),
129
+ u_rho[0][0] * c.conjugate() + u_rho[0][1] * d.conjugate(),
130
+ ],
131
+ [
132
+ u_rho[1][0] * a.conjugate() + u_rho[1][1] * b.conjugate(),
133
+ u_rho[1][0] * c.conjugate() + u_rho[1][1] * d.conjugate(),
134
+ ],
135
+ ]
136
+
137
+ # Extract Bloch components: Tr(ρ'σi) = 2 * Re[...]
138
+ new_x = 2 * rho_prime[0][1].real + 2 * rho_prime[1][0].real
139
+ new_y = 2 * (rho_prime[0][1].imag - rho_prime[1][0].imag)
140
+ new_z = 2 * rho_prime[0][0].real - 1 # since Tr(ρ') = 1
141
+
142
+ p = math.sqrt(new_x**2 + new_y**2 + new_z**2)
143
+
144
+ new_x /= p
145
+ new_y /= p
146
+ new_z /= p
147
+
148
+ self.bloch = [new_x, new_y, new_z]
149
+
150
+ def prob(self, basis=Pauli.PauliZ):
151
+ """Sample a classical outcome from the current 'quantum' state"""
152
+ if basis == Pauli.PauliZ:
153
+ prob_1 = (1 - self.bloch[2]) / 2
154
+ elif basis == Pauli.PauliX:
155
+ prob_1 = (1 - self.bloch[0]) / 2
156
+ elif basis == Pauli.PauliY:
157
+ prob_1 = (1 - self.bloch[1]) / 2
158
+ else:
159
+ raise ValueError(f"Unsupported basis: {basis}")
160
+ return prob_1
161
+
162
+ def m(self):
163
+ result = random.random() < self.prob()
164
+ self.reset()
165
+ if result:
166
+ self.x()
167
+ return result
168
+
169
+
170
+ # Provided by Elara (the custom OpenAI GPT)
171
+ def _cpauli_lhv(prob, targ, axis, anti, theta=math.pi):
172
+ """
173
+ Apply a 'soft' controlled-Pauli gate: rotate target qubit
174
+ proportionally to control's Z expectation value.
175
+
176
+ theta: full rotation angle if control in |1⟩
177
+ """
178
+ # Control influence is (1 - ctrl.bloch[2]) / 2 = P(|1⟩)
179
+ # BUT we avoid collapse by using the expectation value:
180
+ control_influence = (1 - prob) if anti else prob
181
+
182
+ effective_theta = control_influence * theta
183
+
184
+ # Apply partial rotation to target qubit:
185
+ if axis == Pauli.PauliX:
186
+ targ.rx(effective_theta)
187
+ elif axis == Pauli.PauliY:
188
+ targ.ry(effective_theta)
189
+ elif axis == Pauli.PauliZ:
190
+ targ.rz(effective_theta)
191
+
192
+
193
+ class QrackAceBackend:
194
+ """A back end for elided quantum error correction
195
+
196
+ This back end uses elided repetition code on a nearest-neighbor topology to emulate
197
+ a utility-scale superconducting chip quantum computer in very little memory.4
198
+
199
+ The backend was originally designed assuming an (orbifolded) 2D qubit grid like 2019 Sycamore.
200
+ However, it quickly became apparent that users can basically design their own connectivity topologies,
201
+ without breaking the concept. (Not all will work equally well.)
202
+
203
+ Consider distributing the different "patches" to different GPUs with self.sim[sim_id].set_device(gpu_id)!
204
+ (If you have 3+ patches, maybe your discrete GPU can do multiple patches in the time it takes an Intel HD
205
+ to do one patch worth of work!)
206
+
207
+ Attributes:
208
+ sim(QrackSimulator): Array of simulators corresponding to "patches" between boundary rows.
209
+ long_range_columns(int): How many ideal rows between QEC boundary rows?
210
+ is_transpose(bool): Rows are long if False, columns are long if True
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ qubit_count=1,
216
+ long_range_columns=4,
217
+ long_range_rows=4,
218
+ is_transpose=False,
219
+ isTensorNetwork=False,
220
+ isSchmidtDecomposeMulti=False,
221
+ isSchmidtDecompose=True,
222
+ isStabilizerHybrid=False,
223
+ isBinaryDecisionTree=False,
224
+ isPaged=True,
225
+ isCpuGpuHybrid=True,
226
+ isOpenCL=True,
227
+ isHostPointer=(True if os.environ.get("PYQRACK_HOST_POINTER_DEFAULT_ON") else False),
228
+ noise=0,
229
+ toClone=None,
230
+ ):
231
+ if toClone:
232
+ qubit_count = toClone.num_qubits()
233
+ long_range_columns = toClone.long_range_columns
234
+ long_range_rows = toClone.long_range_rows
235
+ is_transpose = toClone.is_transpose
236
+ if qubit_count < 0:
237
+ qubit_count = 0
238
+ if long_range_columns < 0:
239
+ long_range_columns = 0
240
+
241
+ self._factor_width(qubit_count, is_transpose)
242
+ self.long_range_columns = long_range_columns
243
+ self.long_range_rows = long_range_rows
244
+ self.is_transpose = is_transpose
245
+
246
+ fppow = 5
247
+ if "QRACK_FPPOW" in os.environ:
248
+ fppow = int(os.environ.get("QRACK_FPPOW"))
249
+ if fppow < 5:
250
+ self._epsilon = 2**-9
251
+ elif fppow > 5:
252
+ self._epsilon = 2**-51
253
+ else:
254
+ self._epsilon = 2**-22
255
+
256
+ self._coupling_map = None
257
+
258
+ # If there's only one or zero "False" columns or rows,
259
+ # the entire simulator is connected, anyway.
260
+ len_col_seq = long_range_columns + 1
261
+ col_patch_count = (self._row_length + len_col_seq - 1) // len_col_seq
262
+ if (self._row_length < 3) or ((long_range_columns + 1) >= self._row_length):
263
+ self._is_col_long_range = [True] * self._row_length
264
+ else:
265
+ col_seq = [True] * long_range_columns + [False]
266
+ self._is_col_long_range = (col_seq * col_patch_count)[: self._row_length]
267
+ if long_range_columns < self._row_length:
268
+ self._is_col_long_range[-1] = False
269
+ len_row_seq = long_range_rows + 1
270
+ row_patch_count = (self._col_length + len_row_seq - 1) // len_row_seq
271
+ if (self._col_length < 3) or ((long_range_rows + 1) >= self._col_length):
272
+ self._is_row_long_range = [True] * self._col_length
273
+ else:
274
+ row_seq = [True] * long_range_rows + [False]
275
+ self._is_row_long_range = (row_seq * row_patch_count)[: self._col_length]
276
+ if long_range_rows < self._col_length:
277
+ self._is_row_long_range[-1] = False
278
+ sim_count = col_patch_count * row_patch_count
279
+
280
+ self._qubits = []
281
+ sim_counts = [0] * sim_count
282
+ sim_id = 0
283
+ tot_qubits = 0
284
+ for r in self._is_row_long_range:
285
+ for c in self._is_col_long_range:
286
+ qubit = [(sim_id, sim_counts[sim_id])]
287
+ sim_counts[sim_id] += 1
288
+
289
+ if (not c) or (not r):
290
+ t_sim_id = (sim_id + 1) % sim_count
291
+ qubit.append((t_sim_id, sim_counts[t_sim_id]))
292
+ sim_counts[t_sim_id] += 1
293
+
294
+ qubit.append(
295
+ LHVQubit(toClone=(toClone._qubits[tot_qubits][2] if toClone else None))
296
+ )
297
+
298
+ if (not c) and (not r):
299
+ t_sim_id = (sim_id + col_patch_count) % sim_count
300
+ qubit.append((t_sim_id, sim_counts[t_sim_id]))
301
+ sim_counts[t_sim_id] += 1
302
+
303
+ t_sim_id = (t_sim_id + 1) % sim_count
304
+ qubit.append((t_sim_id, sim_counts[t_sim_id]))
305
+ sim_counts[t_sim_id] += 1
306
+
307
+ if not c:
308
+ sim_id = (sim_id + 1) % sim_count
309
+
310
+ self._qubits.append(qubit)
311
+ tot_qubits += 1
312
+
313
+ self.sim = []
314
+ for i in range(sim_count):
315
+ self.sim.append(
316
+ toClone.sim[i].clone()
317
+ if toClone
318
+ else QrackSimulator(
319
+ sim_counts[i],
320
+ isTensorNetwork=isTensorNetwork,
321
+ isSchmidtDecomposeMulti=isSchmidtDecomposeMulti,
322
+ isSchmidtDecompose=isSchmidtDecompose,
323
+ isStabilizerHybrid=isStabilizerHybrid,
324
+ isBinaryDecisionTree=isBinaryDecisionTree,
325
+ isPaged=isPaged,
326
+ isCpuGpuHybrid=isCpuGpuHybrid,
327
+ isOpenCL=isOpenCL,
328
+ isHostPointer=isHostPointer,
329
+ noise=noise,
330
+ )
331
+ )
332
+
333
+ # You can still "monkey-patch" this, after the constructor.
334
+ if "QRACK_QUNIT_SEPARABILITY_THRESHOLD" not in os.environ:
335
+ # (1 - 1 / sqrt(2)) / 4 (but empirically tuned)
336
+ self.sim[i].set_sdrp(0.073223304703363119)
337
+
338
+ def clone(self):
339
+ return QrackAceBackend(toClone=self)
340
+
341
+ def num_qubits(self):
342
+ return self._row_length * self._col_length
343
+
344
+ def get_row_length(self):
345
+ return self._row_length
346
+
347
+ def get_column_length(self):
348
+ return self._col_length
349
+
350
+ def _factor_width(self, width, is_transpose=False):
351
+ col_len = math.floor(math.sqrt(width))
352
+ while ((width // col_len) * col_len) != width:
353
+ col_len -= 1
354
+ row_len = width // col_len
355
+
356
+ self._col_length, self._row_length = (
357
+ (row_len, col_len) if is_transpose else (col_len, row_len)
358
+ )
359
+
360
+ def _ct_pair_prob(self, q1, q2):
361
+ p1 = self.sim[q1[0]].prob(q1[1]) if isinstance(q1, tuple) else q1.prob()
362
+ p2 = self.sim[q2[0]].prob(q2[1]) if isinstance(q2, tuple) else q2.prob()
363
+
364
+ if p1 < p2:
365
+ return p2, q1
366
+
367
+ return p1, q2
368
+
369
+ def _cz_shadow(self, q1, q2):
370
+ prob_max, t = self._ct_pair_prob(q1, q2)
371
+ if prob_max > 0.5:
372
+ if isinstance(t, tuple):
373
+ self.sim[t[0]].z(t[1])
374
+ else:
375
+ t.z()
376
+
377
+ def _qec_x(self, c):
378
+ if isinstance(c, tuple):
379
+ self.sim[c[0]].x(c[1])
380
+ else:
381
+ c.x()
382
+
383
+ def _qec_h(self, t):
384
+ if isinstance(t, tuple):
385
+ self.sim[t[0]].h(t[1])
386
+ else:
387
+ t.h()
388
+
389
+ def _qec_s(self, t):
390
+ if isinstance(t, tuple):
391
+ self.sim[t[0]].s(t[1])
392
+ else:
393
+ t.s()
394
+
395
+ def _qec_adjs(self, t):
396
+ if isinstance(t, tuple):
397
+ self.sim[t[0]].adjs(t[1])
398
+ else:
399
+ t.adjs()
400
+
401
+ def _anti_cz_shadow(self, c, t):
402
+ self._qec_x(c)
403
+ self._cz_shadow(c, t)
404
+ self._qec_x(c)
405
+
406
+ def _cx_shadow(self, c, t):
407
+ self._qec_h(t)
408
+ self._cz_shadow(c, t)
409
+ self._qec_h(t)
410
+
411
+ def _anti_cx_shadow(self, c, t):
412
+ self._qec_x(c)
413
+ self._cx_shadow(c, t)
414
+ self._qec_x(c)
415
+
416
+ def _cy_shadow(self, c, t):
417
+ self._qec_adjs(t)
418
+ self._cx_shadow(c, t)
419
+ self._qec_s(t)
420
+
421
+ def _anti_cy_shadow(self, c, t):
422
+ self._qec_x(c)
423
+ self._cy_shadow(c, t)
424
+ self._qec_x(c)
425
+
426
+ def _unpack(self, lq):
427
+ return self._qubits[lq]
428
+
429
+ @staticmethod
430
+ def _get_qb_lhv_indices(hq):
431
+ qb = []
432
+ if len(hq) < 2:
433
+ qb = [0]
434
+ lhv = -1
435
+ elif len(hq) < 4:
436
+ qb = [0, 1]
437
+ lhv = 2
438
+ else:
439
+ qb = [0, 1, 3, 4]
440
+ lhv = 2
441
+
442
+ return qb, lhv
443
+
444
+ @staticmethod
445
+ def _get_lhv_bloch_angles(sim):
446
+ # Z axis
447
+ z = sim.bloch[2]
448
+
449
+ # X axis
450
+ x = sim.bloch[0]
451
+
452
+ # Y axis
453
+ y = sim.bloch[1]
454
+
455
+ inclination = math.atan2(math.sqrt(x**2 + y**2), z)
456
+ azimuth = math.atan2(y, x)
457
+
458
+ return azimuth, inclination
459
+
460
+ def _get_bloch_angles(self, hq):
461
+ sim = self.sim[hq[0]].clone()
462
+ q = hq[1]
463
+ sim.separate([q])
464
+
465
+ # Z axis
466
+ z = 1 - 2 * sim.prob(q)
467
+
468
+ # X axis
469
+ sim.h(q)
470
+ x = 1 - 2 * sim.prob(q)
471
+ sim.h(q)
472
+
473
+ # Y axis
474
+ sim.adjs(q)
475
+ sim.h(q)
476
+ y = 1 - 2 * sim.prob(q)
477
+ sim.h(q)
478
+ sim.s(q)
479
+
480
+ inclination = math.atan2(math.sqrt(x**2 + y**2), z)
481
+ azimuth = math.atan2(y, x)
482
+
483
+ return azimuth, inclination
484
+
485
+ def _rotate_to_bloch(self, hq, delta_azimuth, delta_inclination):
486
+ sim = self.sim[hq[0]]
487
+ q = hq[1]
488
+
489
+ # Apply rotation as "Azimuth, Inclination" (AI)
490
+ cosA = math.cos(delta_azimuth)
491
+ sinA = math.sin(delta_azimuth)
492
+ cosI = math.cos(delta_inclination / 2)
493
+ sinI = math.sin(delta_inclination / 2)
494
+
495
+ m00 = complex(cosI, 0)
496
+ m01 = complex(-cosA, sinA) * sinI
497
+ m10 = complex(cosA, sinA) * sinI
498
+ m11 = complex(cosI, 0)
499
+
500
+ sim.mtrx([m00, m01, m10, m11], q)
501
+
502
+ @staticmethod
503
+ def _rotate_lhv_to_bloch(sim, delta_azimuth, delta_inclination):
504
+ # Apply rotation as "Azimuth, Inclination" (AI)
505
+ cosA = math.cos(delta_azimuth)
506
+ sinA = math.sin(delta_azimuth)
507
+ cosI = math.cos(delta_inclination / 2)
508
+ sinI = math.sin(delta_inclination / 2)
509
+
510
+ m00 = complex(cosI, 0)
511
+ m01 = complex(-cosA, sinA) * sinI
512
+ m10 = complex(cosA, sinA) * sinI
513
+ m11 = complex(cosI, 0)
514
+
515
+ sim.mtrx([m00, m01, m10, m11])
516
+
517
+ def _correct(self, lq, phase=False, skip_rotation=False):
518
+ hq = self._unpack(lq)
519
+
520
+ if len(hq) == 1:
521
+ return
522
+
523
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
524
+
525
+ if phase:
526
+ for q in qb:
527
+ b = hq[q]
528
+ self.sim[b[0]].h(b[1])
529
+ b = hq[lhv]
530
+ b.h()
531
+
532
+ if len(hq) == 5:
533
+ # RMS
534
+ p = [
535
+ self.sim[hq[0][0]].prob(hq[0][1]),
536
+ self.sim[hq[1][0]].prob(hq[1][1]),
537
+ hq[2].prob(),
538
+ self.sim[hq[3][0]].prob(hq[3][1]),
539
+ self.sim[hq[4][0]].prob(hq[4][1]),
540
+ ]
541
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
542
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + 3 * (p[2] ** 2) + p[3] ** 2 + p[4] ** 2) / 7)
543
+ qrms = math.sqrt(
544
+ (
545
+ (1 - p[0]) ** 2
546
+ + (1 - p[1]) ** 2
547
+ + 3 * ((1 - p[2]) ** 2)
548
+ + (1 - p[3]) ** 2
549
+ + (1 - p[4]) ** 2
550
+ )
551
+ / 7
552
+ )
553
+ result = ((prms + (1 - qrms)) / 2) >= 0.5
554
+ syndrome = (
555
+ [1 - p[0], 1 - p[1], 1 - p[2], 1 - p[3], 1 - p[4]]
556
+ if result
557
+ else [p[0], p[1], p[2], p[3], p[4]]
558
+ )
559
+ for q in range(5):
560
+ if syndrome[q] > (0.5 + self._epsilon):
561
+ if q == 2:
562
+ hq[q].x()
563
+ else:
564
+ self.sim[hq[q][0]].x(hq[q][1])
565
+
566
+ if not skip_rotation:
567
+ a, i = [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]
568
+ a[0], i[0] = self._get_bloch_angles(hq[0])
569
+ a[1], i[1] = self._get_bloch_angles(hq[1])
570
+ a[2], i[2] = QrackAceBackend._get_lhv_bloch_angles(hq[2])
571
+ a[3], i[3] = self._get_bloch_angles(hq[3])
572
+ a[4], i[4] = self._get_bloch_angles(hq[4])
573
+
574
+ a_target = 0
575
+ i_target = 0
576
+ for x in range(5):
577
+ if x == 2:
578
+ continue
579
+ a_target += a[x]
580
+ i_target += i[x]
581
+
582
+ a_target /= 5
583
+ i_target /= 5
584
+ for x in range(5):
585
+ if x == 2:
586
+ QrackAceBackend._rotate_lhv_to_bloch(
587
+ hq[x], a_target - a[x], i_target - i[x]
588
+ )
589
+ else:
590
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
591
+
592
+ else:
593
+ # RMS
594
+ p = [
595
+ self.sim[hq[0][0]].prob(hq[0][1]),
596
+ self.sim[hq[1][0]].prob(hq[1][1]),
597
+ hq[2].prob(),
598
+ ]
599
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
600
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
601
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
602
+ result = ((prms + (1 - qrms)) / 2) >= 0.5
603
+ syndrome = [1 - p[0], 1 - p[1], 1 - p[2]] if result else [p[0], p[1], p[2]]
604
+ for q in range(3):
605
+ if syndrome[q] > (0.5 + self._epsilon):
606
+ if q == 2:
607
+ hq[q].x()
608
+ else:
609
+ self.sim[hq[q][0]].x(hq[q][1])
610
+
611
+ if not skip_rotation:
612
+ a, i = [0, 0, 0], [0, 0, 0]
613
+ a[0], i[0] = self._get_bloch_angles(hq[0])
614
+ a[1], i[1] = self._get_bloch_angles(hq[1])
615
+ a[2], i[2] = QrackAceBackend._get_lhv_bloch_angles(hq[2])
616
+
617
+ a_target = 0
618
+ i_target = 0
619
+ for x in range(3):
620
+ if x == 2:
621
+ continue
622
+ a_target += a[x]
623
+ i_target += i[x]
624
+
625
+ a_target /= 3
626
+ i_target /= 3
627
+ for x in range(3):
628
+ if x == 2:
629
+ QrackAceBackend._rotate_lhv_to_bloch(
630
+ hq[x], a_target - a[x], i_target - i[x]
631
+ )
632
+ else:
633
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
634
+
635
+ if phase:
636
+ for q in qb:
637
+ b = hq[q]
638
+ self.sim[b[0]].h(b[1])
639
+ b = hq[lhv]
640
+ b.h()
641
+
642
+ def apply_magnetic_bias(self, q, b):
643
+ if b == 0:
644
+ return
645
+ b = math.exp(b)
646
+ for x in q:
647
+ hq = self._unpack(x)
648
+ for c in range(len(hq)):
649
+ h = hq[c]
650
+ if c == 2:
651
+ a, i = QrackAceBackend._get_lhv_bloch_angles(h)
652
+ QrackAceBackend._rotate_lhv_to_bloch(
653
+ h,
654
+ math.atan(math.tan(a) * b) - a,
655
+ math.atan(math.tan(i) * b) - i,
656
+ )
657
+ else:
658
+ a, i = self._get_bloch_angles(h)
659
+ self._rotate_to_bloch(
660
+ h,
661
+ math.atan(math.tan(a) * b) - a,
662
+ math.atan(math.tan(i) * b) - i,
663
+ )
664
+
665
+ def u(self, lq, th, ph, lm):
666
+ hq = self._unpack(lq)
667
+ if len(hq) < 2:
668
+ b = hq[0]
669
+ self.sim[b[0]].u(b[1], th, ph, lm)
670
+ return
671
+
672
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
673
+
674
+ for q in qb:
675
+ b = hq[q]
676
+ self.sim[b[0]].u(b[1], th, ph, lm)
677
+
678
+ b = hq[lhv]
679
+ b.u(th, ph, lm)
680
+
681
+ self._correct(lq, False, True)
682
+ self._correct(lq, True, False)
683
+
684
+ def r(self, p, th, lq):
685
+ hq = self._unpack(lq)
686
+ if len(hq) < 2:
687
+ b = hq[0]
688
+ self.sim[b[0]].r(p, th, b[1])
689
+ return
690
+
691
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
692
+
693
+ for q in qb:
694
+ b = hq[q]
695
+ self.sim[b[0]].r(p, th, b[1])
696
+
697
+ b = hq[lhv]
698
+ if p == Pauli.PauliX:
699
+ b.rx(th)
700
+ elif p == Pauli.PauliY:
701
+ b.ry(th)
702
+ elif p == Pauli.PauliZ:
703
+ b.rz(th)
704
+
705
+ if p != Pauli.PauliZ:
706
+ self._correct(lq, False, p != Pauli.PauliX)
707
+ if p != Pauli.PauliX:
708
+ self._correct(lq, True)
709
+
710
+ def h(self, lq):
711
+ hq = self._unpack(lq)
712
+ if len(hq) < 2:
713
+ b = hq[0]
714
+ self.sim[b[0]].h(b[1])
715
+ return
716
+
717
+ self._correct(lq)
718
+
719
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
720
+
721
+ for q in qb:
722
+ b = hq[q]
723
+ self.sim[b[0]].h(b[1])
724
+
725
+ b = hq[lhv]
726
+ b.h()
727
+
728
+ self._correct(lq)
729
+
730
+ def s(self, lq):
731
+ hq = self._unpack(lq)
732
+ if len(hq) < 2:
733
+ b = hq[0]
734
+ self.sim[b[0]].s(b[1])
735
+ return
736
+
737
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
738
+
739
+ for q in qb:
740
+ b = hq[q]
741
+ self.sim[b[0]].s(b[1])
742
+
743
+ b = hq[lhv]
744
+ b.s()
745
+
746
+ def adjs(self, lq):
747
+ hq = self._unpack(lq)
748
+ if len(hq) < 2:
749
+ b = hq[0]
750
+ self.sim[b[0]].adjs(b[1])
751
+ return
752
+
753
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
754
+
755
+ for q in qb:
756
+ b = hq[q]
757
+ self.sim[b[0]].adjs(b[1])
758
+
759
+ b = hq[lhv]
760
+ b.adjs()
761
+
762
+ def x(self, lq):
763
+ hq = self._unpack(lq)
764
+ if len(hq) < 2:
765
+ b = hq[0]
766
+ self.sim[b[0]].x(b[1])
767
+ return
768
+
769
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
770
+
771
+ for q in qb:
772
+ b = hq[q]
773
+ self.sim[b[0]].x(b[1])
774
+
775
+ b = hq[lhv]
776
+ b.x()
777
+
778
+ def y(self, lq):
779
+ hq = self._unpack(lq)
780
+ if len(hq) < 2:
781
+ b = hq[0]
782
+ self.sim[b[0]].y(b[1])
783
+ return
784
+
785
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
786
+
787
+ for q in qb:
788
+ b = hq[q]
789
+ self.sim[b[0]].y(b[1])
790
+
791
+ b = hq[lhv]
792
+ b.y()
793
+
794
+ def z(self, lq):
795
+ hq = self._unpack(lq)
796
+ if len(hq) < 2:
797
+ b = hq[0]
798
+ self.sim[b[0]].z(b[1])
799
+ return
800
+
801
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
802
+
803
+ for q in qb:
804
+ b = hq[q]
805
+ self.sim[b[0]].z(b[1])
806
+
807
+ b = hq[lhv]
808
+ b.z()
809
+
810
+ def t(self, lq):
811
+ hq = self._unpack(lq)
812
+ if len(hq) < 2:
813
+ b = hq[0]
814
+ self.sim[b[0]].t(b[1])
815
+ return
816
+
817
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
818
+
819
+ for q in qb:
820
+ b = hq[q]
821
+ self.sim[b[0]].t(b[1])
822
+
823
+ b = hq[lhv]
824
+ b.t()
825
+
826
+ def adjt(self, lq):
827
+ hq = self._unpack(lq)
828
+ if len(hq) < 2:
829
+ b = hq[0]
830
+ self.sim[b[0]].adjt(b[1])
831
+ return
832
+
833
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
834
+
835
+ for q in qb:
836
+ b = hq[q]
837
+ self.sim[b[0]].adjt(b[1])
838
+
839
+ b = hq[lhv]
840
+ b.adjt()
841
+
842
+ def _get_gate(self, pauli, anti, sim_id):
843
+ gate = None
844
+ shadow = None
845
+ if pauli == Pauli.PauliX:
846
+ gate = self.sim[sim_id].macx if anti else self.sim[sim_id].mcx
847
+ shadow = self._anti_cx_shadow if anti else self._cx_shadow
848
+ elif pauli == Pauli.PauliY:
849
+ gate = self.sim[sim_id].macy if anti else self.sim[sim_id].mcy
850
+ shadow = self._anti_cy_shadow if anti else self._cy_shadow
851
+ elif pauli == Pauli.PauliZ:
852
+ gate = self.sim[sim_id].macz if anti else self.sim[sim_id].mcz
853
+ shadow = self._anti_cz_shadow if anti else self._cz_shadow
854
+ else:
855
+ raise RuntimeError("QrackAceBackend._get_gate() should never return identity!")
856
+
857
+ return gate, shadow
858
+
859
+ def _get_connected(self, i, is_row):
860
+ long_range = self._is_row_long_range if is_row else self._is_col_long_range
861
+ length = self._col_length if is_row else self._row_length
862
+
863
+ connected = [i]
864
+ c = (i - 1) % length
865
+ while long_range[c] and (len(connected) < length):
866
+ connected.append(c)
867
+ c = (c - 1) % length
868
+ if len(connected) < length:
869
+ connected.append(c)
870
+ boundary = len(connected)
871
+ c = (i + 1) % length
872
+ while long_range[c] and (len(connected) < length):
873
+ connected.append(c)
874
+ c = (c + 1) % length
875
+ if len(connected) < length:
876
+ connected.append(c)
877
+
878
+ return connected, boundary
879
+
880
+ def _apply_coupling(self, pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr):
881
+ for q1 in qb1:
882
+ if q1 == lhv1:
883
+ continue
884
+ b1 = hq1[q1]
885
+ gate_fn, shadow_fn = self._get_gate(pauli, anti, b1[0])
886
+ for q2 in qb2:
887
+ if q2 == lhv2:
888
+ continue
889
+ b2 = hq2[q2]
890
+ if b1[0] == b2[0]:
891
+ gate_fn([b1[1]], b2[1])
892
+ elif lq1_lr or (b1[1] == b2[1]) or ((len(qb1) == 2) and (b1[1] == (b2[1] & 1))):
893
+ shadow_fn(b1, b2)
894
+
895
+ def _cpauli(self, lq1, lq2, anti, pauli):
896
+ lq1_row = lq1 // self._row_length
897
+ lq1_col = lq1 % self._row_length
898
+ lq2_row = lq2 // self._row_length
899
+ lq2_col = lq2 % self._row_length
900
+
901
+ hq1 = self._unpack(lq1)
902
+ hq2 = self._unpack(lq2)
903
+
904
+ lq1_lr = len(hq1) == 1
905
+ lq2_lr = len(hq2) == 1
906
+
907
+ self._correct(lq1)
908
+
909
+ qb1, lhv1 = QrackAceBackend._get_qb_lhv_indices(hq1)
910
+ qb2, lhv2 = QrackAceBackend._get_qb_lhv_indices(hq2)
911
+ # Apply cross coupling on hardware qubits first
912
+ self._apply_coupling(pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr)
913
+ # Apply coupling to the local-hidden-variable target
914
+ if lhv2 >= 0:
915
+ _cpauli_lhv(
916
+ hq1[lhv1].prob() if lhv1 >= 0 else self.sim[hq1[0][0]].prob(hq1[0][1]),
917
+ hq2[lhv2],
918
+ pauli,
919
+ anti,
920
+ )
921
+
922
+ self._correct(lq1, True)
923
+ if pauli != Pauli.PauliZ:
924
+ self._correct(lq2, False, pauli != Pauli.PauliX)
925
+ if pauli != Pauli.PauliX:
926
+ self._correct(lq2, True)
927
+
928
+ def cx(self, lq1, lq2):
929
+ self._cpauli(lq1, lq2, False, Pauli.PauliX)
930
+
931
+ def cy(self, lq1, lq2):
932
+ self._cpauli(lq1, lq2, False, Pauli.PauliY)
933
+
934
+ def cz(self, lq1, lq2):
935
+ self._cpauli(lq1, lq2, False, Pauli.PauliZ)
936
+
937
+ def acx(self, lq1, lq2):
938
+ self._cpauli(lq1, lq2, True, Pauli.PauliX)
939
+
940
+ def acy(self, lq1, lq2):
941
+ self._cpauli(lq1, lq2, True, Pauli.PauliY)
942
+
943
+ def acz(self, lq1, lq2):
944
+ self._cpauli(lq1, lq2, True, Pauli.PauliZ)
945
+
946
+ def mcx(self, lq1, lq2):
947
+ if len(lq1) > 1:
948
+ raise RuntimeError(
949
+ "QrackAceBackend.mcx() is provided for syntax convenience and only supports 1 control qubit!"
950
+ )
951
+ self._cpauli(lq1[0], lq2, False, Pauli.PauliX)
952
+
953
+ def mcy(self, lq1, lq2):
954
+ if len(lq1) > 1:
955
+ raise RuntimeError(
956
+ "QrackAceBackend.mcy() is provided for syntax convenience and only supports 1 control qubit!"
957
+ )
958
+ self._cpauli(lq1[0], lq2, False, Pauli.PauliY)
959
+
960
+ def mcz(self, lq1, lq2):
961
+ if len(lq1) > 1:
962
+ raise RuntimeError(
963
+ "QrackAceBackend.mcz() is provided for syntax convenience and only supports 1 control qubit!"
964
+ )
965
+ self._cpauli(lq1[0], lq2, False, Pauli.PauliZ)
966
+
967
+ def macx(self, lq1, lq2):
968
+ if len(lq1) > 1:
969
+ raise RuntimeError(
970
+ "QrackAceBackend.macx() is provided for syntax convenience and only supports 1 control qubit!"
971
+ )
972
+ self._cpauli(lq1[0], lq2, True, Pauli.PauliX)
973
+
974
+ def macy(self, lq1, lq2):
975
+ if len(lq1) > 1:
976
+ raise RuntimeError(
977
+ "QrackAceBackend.macy() is provided for syntax convenience and only supports 1 control qubit!"
978
+ )
979
+ self._cpauli(lq1[0], lq2, True, Pauli.PauliY)
980
+
981
+ def macz(self, lq1, lq2):
982
+ if len(lq1) > 1:
983
+ raise RuntimeError(
984
+ "QrackAceBackend.macz() is provided for syntax convenience and only supports 1 control qubit!"
985
+ )
986
+ self._cpauli(lq1[0], lq2, True, Pauli.PauliZ)
987
+
988
+ def swap(self, lq1, lq2):
989
+ self.cx(lq1, lq2)
990
+ self.cx(lq2, lq1)
991
+ self.cx(lq1, lq2)
992
+
993
+ def iswap(self, lq1, lq2):
994
+ self.swap(lq1, lq2)
995
+ self.cz(lq1, lq2)
996
+ self.s(lq1)
997
+ self.s(lq2)
998
+
999
+ def adjiswap(self, lq1, lq2):
1000
+ self.adjs(lq2)
1001
+ self.adjs(lq1)
1002
+ self.cz(lq1, lq2)
1003
+ self.swap(lq1, lq2)
1004
+
1005
+ def prob(self, lq):
1006
+ hq = self._unpack(lq)
1007
+ if len(hq) < 2:
1008
+ b = hq[0]
1009
+ return self.sim[b[0]].prob(b[1])
1010
+
1011
+ self._correct(lq)
1012
+ if len(hq) == 5:
1013
+ # RMS
1014
+ p = [
1015
+ self.sim[hq[0][0]].prob(hq[0][1]),
1016
+ self.sim[hq[1][0]].prob(hq[1][1]),
1017
+ hq[2].prob(),
1018
+ self.sim[hq[3][0]].prob(hq[3][1]),
1019
+ self.sim[hq[4][0]].prob(hq[4][1]),
1020
+ ]
1021
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
1022
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + 3 * (p[2] ** 2) + p[3] ** 2 + p[4] ** 2) / 7)
1023
+ qrms = math.sqrt(
1024
+ (
1025
+ (1 - p[0]) ** 2
1026
+ + (1 - p[1]) ** 2
1027
+ + 3 * ((1 - p[2]) ** 2)
1028
+ + (1 - p[3]) ** 2
1029
+ + (1 - p[4]) ** 2
1030
+ )
1031
+ / 7
1032
+ )
1033
+ else:
1034
+ # RMS
1035
+ p = [
1036
+ self.sim[hq[0][0]].prob(hq[0][1]),
1037
+ self.sim[hq[1][0]].prob(hq[1][1]),
1038
+ hq[2].prob(),
1039
+ ]
1040
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
1041
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
1042
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
1043
+
1044
+ return (prms + (1 - qrms)) / 2
1045
+
1046
+ def m(self, lq):
1047
+ hq = self._unpack(lq)
1048
+ if len(hq) < 2:
1049
+ b = hq[0]
1050
+ return self.sim[b[0]].m(b[1])
1051
+
1052
+ p = self.prob(lq)
1053
+ result = ((p + self._epsilon) >= 1) or (random.random() < p)
1054
+
1055
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1056
+
1057
+ for q in qb:
1058
+ b = hq[q]
1059
+ p = self.sim[b[0]].prob(b[1]) if result else (1 - self.sim[b[0]].prob(b[1]))
1060
+ if p < self._epsilon:
1061
+ if self.sim[b[0]].m(b[1]) != result:
1062
+ self.sim[b[0]].x(b[1])
1063
+ else:
1064
+ self.sim[b[0]].force_m(b[1], result)
1065
+
1066
+ b = hq[lhv]
1067
+ b.reset()
1068
+ if result:
1069
+ b.x()
1070
+
1071
+ return result
1072
+
1073
+ def force_m(self, lq, result):
1074
+ hq = self._unpack(lq)
1075
+ if len(hq) < 2:
1076
+ b = hq[0]
1077
+ return self.sim[b[0]].force_m(b[1], result)
1078
+
1079
+ self._correct(lq)
1080
+
1081
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1082
+
1083
+ for q in qb:
1084
+ b = hq[q]
1085
+ p = self.sim[b[0]].prob(b[1]) if result else (1 - self.sim[b[0]].prob(b[1]))
1086
+ if p < self._epsilon:
1087
+ if self.sim[b[0]].m(b[1]) != result:
1088
+ self.sim[b[0]].x(b[1])
1089
+ else:
1090
+ self.sim[b[0]].force_m(b[1], result)
1091
+
1092
+ b = hq[1]
1093
+ b.reset()
1094
+ if result:
1095
+ b.x()
1096
+
1097
+ return c
1098
+
1099
+ def m_all(self):
1100
+ # Randomize the order of measurement to amortize error.
1101
+ result = 0
1102
+ rows = list(range(self._col_length))
1103
+ random.shuffle(rows)
1104
+ for lq_row in rows:
1105
+ row_offset = lq_row * self._row_length
1106
+ cols = list(range(self._row_length))
1107
+ random.shuffle(cols)
1108
+ for lq_col in cols:
1109
+ lq = row_offset + lq_col
1110
+ if self.m(lq):
1111
+ result |= 1 << lq
1112
+
1113
+ return result
1114
+
1115
+ def measure_shots(self, q, s):
1116
+ samples = []
1117
+ for _ in range(s):
1118
+ clone = self.clone()
1119
+ _sample = clone.m_all()
1120
+ sample = 0
1121
+ for i in range(len(q)):
1122
+ if (_sample >> q[i]) & 1:
1123
+ sample |= 1 << i
1124
+ samples.append(sample)
1125
+
1126
+ return samples
1127
+
1128
+ def _apply_op(self, operation):
1129
+ name = operation.name
1130
+
1131
+ if (name == "id") or (name == "barrier"):
1132
+ # Skip measurement logic
1133
+ return
1134
+
1135
+ conditional = getattr(operation, "conditional", None)
1136
+ if isinstance(conditional, int):
1137
+ conditional_bit_set = (self._classical_register >> conditional) & 1
1138
+ if not conditional_bit_set:
1139
+ return
1140
+ elif conditional is not None:
1141
+ mask = int(conditional.mask, 16)
1142
+ if mask > 0:
1143
+ value = self._classical_memory & mask
1144
+ while (mask & 0x1) == 0:
1145
+ mask >>= 1
1146
+ value >>= 1
1147
+ if value != int(conditional.val, 16):
1148
+ return
1149
+
1150
+ if (name == "u1") or (name == "p"):
1151
+ self._sim.u(operation.qubits[0]._index, 0, 0, float(operation.params[0]))
1152
+ elif name == "u2":
1153
+ self._sim.u(
1154
+ operation.qubits[0]._index,
1155
+ math.pi / 2,
1156
+ float(operation.params[0]),
1157
+ float(operation.params[1]),
1158
+ )
1159
+ elif (name == "u3") or (name == "u"):
1160
+ self._sim.u(
1161
+ operation.qubits[0]._index,
1162
+ float(operation.params[0]),
1163
+ float(operation.params[1]),
1164
+ float(operation.params[2]),
1165
+ )
1166
+ elif name == "r":
1167
+ self._sim.u(
1168
+ operation.qubits[0]._index,
1169
+ float(operation.params[0]),
1170
+ float(operation.params[1]) - math.pi / 2,
1171
+ (-1 * float(operation.params[1])) + math.pi / 2,
1172
+ )
1173
+ elif name == "rx":
1174
+ self._sim.r(Pauli.PauliX, float(operation.params[0]), operation.qubits[0]._index)
1175
+ elif name == "ry":
1176
+ self._sim.r(Pauli.PauliY, float(operation.params[0]), operation.qubits[0]._index)
1177
+ elif name == "rz":
1178
+ self._sim.r(Pauli.PauliZ, float(operation.params[0]), operation.qubits[0]._index)
1179
+ elif name == "h":
1180
+ self._sim.h(operation.qubits[0]._index)
1181
+ elif name == "x":
1182
+ self._sim.x(operation.qubits[0]._index)
1183
+ elif name == "y":
1184
+ self._sim.y(operation.qubits[0]._index)
1185
+ elif name == "z":
1186
+ self._sim.z(operation.qubits[0]._index)
1187
+ elif name == "s":
1188
+ self._sim.s(operation.qubits[0]._index)
1189
+ elif name == "sdg":
1190
+ self._sim.adjs(operation.qubits[0]._index)
1191
+ elif name == "t":
1192
+ self._sim.t(operation.qubits[0]._index)
1193
+ elif name == "tdg":
1194
+ self._sim.adjt(operation.qubits[0]._index)
1195
+ elif name == "cx":
1196
+ self._sim.cx(operation.qubits[0]._index, operation.qubits[1]._index)
1197
+ elif name == "cy":
1198
+ self._sim.cy(operation.qubits[0]._index, operation.qubits[1]._index)
1199
+ elif name == "cz":
1200
+ self._sim.cz(operation.qubits[0]._index, operation.qubits[1]._index)
1201
+ elif name == "dcx":
1202
+ self._sim.mcx(operation.qubits[0]._index, operation.qubits[1]._index)
1203
+ self._sim.mcx(operation.qubits[1]._index, operation.qubits[0]._index)
1204
+ elif name == "swap":
1205
+ self._sim.swap(operation.qubits[0]._index, operation.qubits[1]._index)
1206
+ elif name == "iswap":
1207
+ self._sim.iswap(operation.qubits[0]._index, operation.qubits[1]._index)
1208
+ elif name == "iswap_dg":
1209
+ self._sim.adjiswap(operation.qubits[0]._index, operation.qubits[1]._index)
1210
+ elif name == "reset":
1211
+ qubits = operation.qubits
1212
+ for qubit in qubits:
1213
+ if self._sim.m(qubit._index):
1214
+ self._sim.x(qubit._index)
1215
+ elif name == "measure":
1216
+ qubits = operation.qubits
1217
+ clbits = operation.clbits
1218
+ cregbits = (
1219
+ operation.register
1220
+ if hasattr(operation, "register")
1221
+ else len(operation.qubits) * [-1]
1222
+ )
1223
+
1224
+ self._sample_qubits += qubits
1225
+ self._sample_clbits += clbits
1226
+ self._sample_cregbits += cregbits
1227
+
1228
+ if not self._sample_measure:
1229
+ for index in range(len(qubits)):
1230
+ qubit_outcome = self._sim.m(qubits[index]._index)
1231
+
1232
+ clbit = clbits[index]
1233
+ clmask = 1 << clbit
1234
+ self._classical_memory = (self._classical_memory & (~clmask)) | (
1235
+ qubit_outcome << clbit
1236
+ )
1237
+
1238
+ cregbit = cregbits[index]
1239
+ if cregbit < 0:
1240
+ cregbit = clbit
1241
+
1242
+ regbit = 1 << cregbit
1243
+ self._classical_register = (self._classical_register & (~regbit)) | (
1244
+ qubit_outcome << cregbit
1245
+ )
1246
+
1247
+ elif name == "bfunc":
1248
+ mask = int(operation.mask, 16)
1249
+ relation = operation.relation
1250
+ val = int(operation.val, 16)
1251
+
1252
+ cregbit = operation.register
1253
+ cmembit = operation.memory if hasattr(operation, "memory") else None
1254
+
1255
+ compared = (self._classical_register & mask) - val
1256
+
1257
+ if relation == "==":
1258
+ outcome = compared == 0
1259
+ elif relation == "!=":
1260
+ outcome = compared != 0
1261
+ elif relation == "<":
1262
+ outcome = compared < 0
1263
+ elif relation == "<=":
1264
+ outcome = compared <= 0
1265
+ elif relation == ">":
1266
+ outcome = compared > 0
1267
+ elif relation == ">=":
1268
+ outcome = compared >= 0
1269
+ else:
1270
+ raise QrackError("Invalid boolean function relation.")
1271
+
1272
+ # Store outcome in register and optionally memory slot
1273
+ regbit = 1 << cregbit
1274
+ self._classical_register = (self._classical_register & (~regbit)) | (
1275
+ int(outcome) << cregbit
1276
+ )
1277
+ if cmembit is not None:
1278
+ membit = 1 << cmembit
1279
+ self._classical_memory = (self._classical_memory & (~membit)) | (
1280
+ int(outcome) << cmembit
1281
+ )
1282
+ else:
1283
+ err_msg = 'QrackAceBackend encountered unrecognized operation "{0}"'
1284
+ raise RuntimeError(err_msg.format(operation))
1285
+
1286
+ def _add_sample_measure(self, sample_qubits, sample_clbits, num_samples):
1287
+ """Generate data samples from current statevector.
1288
+
1289
+ Taken almost straight from the terra source code.
1290
+
1291
+ Args:
1292
+ measure_params (list): List of (qubit, clbit) values for
1293
+ measure instructions to sample.
1294
+ num_samples (int): The number of data samples to generate.
1295
+
1296
+ Returns:
1297
+ list: A list of data values in hex format.
1298
+ """
1299
+ # Get unique qubits that are actually measured
1300
+ measure_qubit = [qubit for qubit in sample_qubits]
1301
+ measure_clbit = [clbit for clbit in sample_clbits]
1302
+
1303
+ # Sample and convert to bit-strings
1304
+ if num_samples == 1:
1305
+ sample = self._sim.m_all()
1306
+ result = 0
1307
+ for index in range(len(measure_qubit)):
1308
+ qubit = measure_qubit[index]._index
1309
+ qubit_outcome = (sample >> qubit) & 1
1310
+ result |= qubit_outcome << index
1311
+ measure_results = [result]
1312
+ else:
1313
+ measure_results = self._sim.measure_shots(
1314
+ [q._index for q in measure_qubit], num_samples
1315
+ )
1316
+
1317
+ data = []
1318
+ for sample in measure_results:
1319
+ for index in range(len(measure_qubit)):
1320
+ qubit_outcome = (sample >> index) & 1
1321
+ clbit = measure_clbit[index]._index
1322
+ clmask = 1 << clbit
1323
+ self._classical_memory = (self._classical_memory & (~clmask)) | (
1324
+ qubit_outcome << clbit
1325
+ )
1326
+
1327
+ data.append(bin(self._classical_memory)[2:].zfill(self.num_qubits()))
1328
+
1329
+ return data
1330
+
1331
+ def run_qiskit_circuit(self, experiment, shots=1):
1332
+ if not _IS_QISKIT_AVAILABLE:
1333
+ raise RuntimeError(
1334
+ "Before trying to run_qiskit_circuit() with QrackAceBackend, you must install Qiskit!"
1335
+ )
1336
+
1337
+ instructions = []
1338
+ if isinstance(experiment, QuantumCircuit):
1339
+ instructions = experiment.data
1340
+ else:
1341
+ raise RuntimeError('Unrecognized "run_input" argument specified for run().')
1342
+
1343
+ self._shots = shots
1344
+ self._sample_qubits = []
1345
+ self._sample_clbits = []
1346
+ self._sample_cregbits = []
1347
+ self._sample_measure = True
1348
+ _data = []
1349
+ shotLoopMax = 1
1350
+
1351
+ is_initializing = True
1352
+ boundary_start = -1
1353
+
1354
+ for opcount in range(len(instructions)):
1355
+ operation = instructions[opcount]
1356
+
1357
+ if operation.name == "id" or operation.name == "barrier":
1358
+ continue
1359
+
1360
+ if is_initializing and ((operation.name == "measure") or (operation.name == "reset")):
1361
+ continue
1362
+
1363
+ is_initializing = False
1364
+
1365
+ if (operation.name == "measure") or (operation.name == "reset"):
1366
+ if boundary_start == -1:
1367
+ boundary_start = opcount
1368
+
1369
+ if (boundary_start != -1) and (operation.name != "measure"):
1370
+ shotsPerLoop = 1
1371
+ shotLoopMax = self._shots
1372
+ self._sample_measure = False
1373
+ break
1374
+
1375
+ preamble_memory = 0
1376
+ preamble_register = 0
1377
+ preamble_sim = None
1378
+
1379
+ if self._sample_measure or boundary_start <= 0:
1380
+ boundary_start = 0
1381
+ self._sample_measure = True
1382
+ shotsPerLoop = self._shots
1383
+ shotLoopMax = 1
1384
+ else:
1385
+ boundary_start -= 1
1386
+ if boundary_start > 0:
1387
+ self._sim = self
1388
+ self._classical_memory = 0
1389
+ self._classical_register = 0
1390
+
1391
+ for operation in instructions[:boundary_start]:
1392
+ self._apply_op(operation)
1393
+
1394
+ preamble_memory = self._classical_memory
1395
+ preamble_register = self._classical_register
1396
+ preamble_sim = self._sim
1397
+
1398
+ for shot in range(shotLoopMax):
1399
+ if preamble_sim is None:
1400
+ self._sim = self
1401
+ self._classical_memory = 0
1402
+ self._classical_register = 0
1403
+ else:
1404
+ self._sim = QrackAceBackend(toClone=preamble_sim)
1405
+ self._classical_memory = preamble_memory
1406
+ self._classical_register = preamble_register
1407
+
1408
+ for operation in instructions[boundary_start:]:
1409
+ self._apply_op(operation)
1410
+
1411
+ if not self._sample_measure and (len(self._sample_qubits) > 0):
1412
+ _data += [bin(self._classical_memory)[2:].zfill(self.num_qubits())]
1413
+ self._sample_qubits = []
1414
+ self._sample_clbits = []
1415
+ self._sample_cregbits = []
1416
+
1417
+ if self._sample_measure and (len(self._sample_qubits) > 0):
1418
+ _data = self._add_sample_measure(self._sample_qubits, self._sample_clbits, self._shots)
1419
+
1420
+ del self._sim
1421
+
1422
+ return _data
1423
+
1424
+ @staticmethod
1425
+ def get_qiskit_basis_gates():
1426
+ return [
1427
+ "id",
1428
+ "u",
1429
+ "u1",
1430
+ "u2",
1431
+ "u3",
1432
+ "r",
1433
+ "rx",
1434
+ "ry",
1435
+ "rz",
1436
+ "h",
1437
+ "x",
1438
+ "y",
1439
+ "z",
1440
+ "s",
1441
+ "sdg",
1442
+ "sx",
1443
+ "sxdg",
1444
+ "p",
1445
+ "t",
1446
+ "tdg",
1447
+ "cx",
1448
+ "cy",
1449
+ "cz",
1450
+ "swap",
1451
+ "iswap",
1452
+ "reset",
1453
+ "measure",
1454
+ ]
1455
+
1456
+ # Mostly written by Dan, but with a little help from Elara (custom OpenAI GPT)
1457
+ def get_logical_coupling_map(self):
1458
+ if self._coupling_map:
1459
+ return self._coupling_map
1460
+
1461
+ coupling_map = set()
1462
+ rows, cols = self._row_length, self._col_length
1463
+
1464
+ # Map each column index to its full list of logical qubit indices
1465
+ def logical_index(row, col):
1466
+ return row * cols + col
1467
+
1468
+ for col in range(cols):
1469
+ connected_cols, _ = self._get_connected(col, False)
1470
+ for row in range(rows):
1471
+ connected_rows, _ = self._get_connected(row, False)
1472
+ a = logical_index(row, col)
1473
+ for c in connected_cols:
1474
+ for r in connected_rows:
1475
+ b = logical_index(r, c)
1476
+ if a != b:
1477
+ coupling_map.add((a, b))
1478
+
1479
+ self._coupling_map = sorted(coupling_map)
1480
+
1481
+ return self._coupling_map
1482
+
1483
+ # Designed by Dan, and implemented by Elara:
1484
+ def create_noise_model(self, x=0.25, y=0.25):
1485
+ if not _IS_QISKIT_AER_AVAILABLE:
1486
+ raise RuntimeError(
1487
+ "Before trying to run_qiskit_circuit() with QrackAceBackend, you must install Qiskit Aer!"
1488
+ )
1489
+ noise_model = NoiseModel()
1490
+
1491
+ for a, b in self.get_logical_coupling_map():
1492
+ col_a, col_b = a % self._row_length, b % self._row_length
1493
+ row_a, row_b = a // self._row_length, b // self._row_length
1494
+ is_long_a = self._is_col_long_range[col_a]
1495
+ is_long_b = self._is_col_long_range[col_b]
1496
+
1497
+ if is_long_a and is_long_b:
1498
+ continue # No noise on long-to-long
1499
+
1500
+ if (col_a == col_b) or (row_a == row_b):
1501
+ continue # No noise for same column
1502
+
1503
+ if is_long_a or is_long_b:
1504
+ y_cy = 1 - (1 - y) ** 2
1505
+ y_swap = 1 - (1 - y) ** 3
1506
+ noise_model.add_quantum_error(depolarizing_error(y, 2), "cx", [a, b])
1507
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cy", [a, b])
1508
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cz", [a, b])
1509
+ noise_model.add_quantum_error(depolarizing_error(y_swap, 2), "swap", [a, b])
1510
+ else:
1511
+ y_cy = 1 - (1 - y) ** 2
1512
+ y_swap = 1 - (1 - y) ** 3
1513
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cx", [a, b])
1514
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cy", [a, b])
1515
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cz", [a, b])
1516
+ noise_model.add_quantum_error(depolarizing_error(y_swap, 2), "swap", [a, b])
1517
+
1518
+ return noise_model