pyqrack-cpu 1.76.0__py3-none-macosx_14_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,1544 @@
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=(
228
+ True if os.environ.get("PYQRACK_HOST_POINTER_DEFAULT_ON") else False
229
+ ),
230
+ noise=0,
231
+ toClone=None,
232
+ ):
233
+ if toClone:
234
+ qubit_count = toClone.num_qubits()
235
+ long_range_columns = toClone.long_range_columns
236
+ long_range_rows = toClone.long_range_rows
237
+ is_transpose = toClone.is_transpose
238
+ if qubit_count < 0:
239
+ qubit_count = 0
240
+ if long_range_columns < 0:
241
+ long_range_columns = 0
242
+
243
+ self._factor_width(qubit_count, is_transpose)
244
+ self.long_range_columns = long_range_columns
245
+ self.long_range_rows = long_range_rows
246
+ self.is_transpose = is_transpose
247
+
248
+ fppow = 5
249
+ if "QRACK_FPPOW" in os.environ:
250
+ fppow = int(os.environ.get("QRACK_FPPOW"))
251
+ if fppow < 5:
252
+ self._epsilon = 2**-9
253
+ elif fppow > 5:
254
+ self._epsilon = 2**-51
255
+ else:
256
+ self._epsilon = 2**-22
257
+
258
+ self._coupling_map = None
259
+
260
+ # If there's only one or zero "False" columns or rows,
261
+ # the entire simulator is connected, anyway.
262
+ len_col_seq = long_range_columns + 1
263
+ col_patch_count = (self._row_length + len_col_seq - 1) // len_col_seq
264
+ if (self._row_length < 3) or ((long_range_columns + 1) >= self._row_length):
265
+ self._is_col_long_range = [True] * self._row_length
266
+ else:
267
+ col_seq = [True] * long_range_columns + [False]
268
+ self._is_col_long_range = (col_seq * col_patch_count)[: self._row_length]
269
+ if long_range_columns < self._row_length:
270
+ self._is_col_long_range[-1] = False
271
+ len_row_seq = long_range_rows + 1
272
+ row_patch_count = (self._col_length + len_row_seq - 1) // len_row_seq
273
+ if (self._col_length < 3) or ((long_range_rows + 1) >= self._col_length):
274
+ self._is_row_long_range = [True] * self._col_length
275
+ else:
276
+ row_seq = [True] * long_range_rows + [False]
277
+ self._is_row_long_range = (row_seq * row_patch_count)[: self._col_length]
278
+ if long_range_rows < self._col_length:
279
+ self._is_row_long_range[-1] = False
280
+ sim_count = col_patch_count * row_patch_count
281
+
282
+ self._qubits = []
283
+ sim_counts = [0] * sim_count
284
+ sim_id = 0
285
+ tot_qubits = 0
286
+ for r in self._is_row_long_range:
287
+ for c in self._is_col_long_range:
288
+ qubit = [(sim_id, sim_counts[sim_id])]
289
+ sim_counts[sim_id] += 1
290
+
291
+ if (not c) or (not r):
292
+ t_sim_id = (sim_id + 1) % sim_count
293
+ qubit.append((t_sim_id, sim_counts[t_sim_id]))
294
+ sim_counts[t_sim_id] += 1
295
+
296
+ qubit.append(
297
+ LHVQubit(
298
+ toClone=(
299
+ toClone._qubits[tot_qubits][2] if toClone else None
300
+ )
301
+ )
302
+ )
303
+
304
+ if (not c) and (not r):
305
+ t_sim_id = (sim_id + col_patch_count) % sim_count
306
+ qubit.append((t_sim_id, sim_counts[t_sim_id]))
307
+ sim_counts[t_sim_id] += 1
308
+
309
+ t_sim_id = (t_sim_id + 1) % sim_count
310
+ qubit.append((t_sim_id, sim_counts[t_sim_id]))
311
+ sim_counts[t_sim_id] += 1
312
+
313
+ if not c:
314
+ sim_id = (sim_id + 1) % sim_count
315
+
316
+ self._qubits.append(qubit)
317
+ tot_qubits += 1
318
+
319
+ self.sim = []
320
+ for i in range(sim_count):
321
+ self.sim.append(
322
+ toClone.sim[i].clone()
323
+ if toClone
324
+ else QrackSimulator(
325
+ sim_counts[i],
326
+ isTensorNetwork=isTensorNetwork,
327
+ isSchmidtDecomposeMulti=isSchmidtDecomposeMulti,
328
+ isSchmidtDecompose=isSchmidtDecompose,
329
+ isStabilizerHybrid=isStabilizerHybrid,
330
+ isBinaryDecisionTree=isBinaryDecisionTree,
331
+ isPaged=isPaged,
332
+ isCpuGpuHybrid=isCpuGpuHybrid,
333
+ isOpenCL=isOpenCL,
334
+ isHostPointer=isHostPointer,
335
+ noise=noise,
336
+ )
337
+ )
338
+
339
+ # You can still "monkey-patch" this, after the constructor.
340
+ if "QRACK_QUNIT_SEPARABILITY_THRESHOLD" not in os.environ:
341
+ # (1 - 1 / sqrt(2)) / 4 (but empirically tuned)
342
+ self.sim[i].set_sdrp(0.073223304703363119)
343
+
344
+ def clone(self):
345
+ return QrackAceBackend(toClone=self)
346
+
347
+ def num_qubits(self):
348
+ return self._row_length * self._col_length
349
+
350
+ def get_row_length(self):
351
+ return self._row_length
352
+
353
+ def get_column_length(self):
354
+ return self._col_length
355
+
356
+ def _factor_width(self, width, is_transpose=False):
357
+ col_len = math.floor(math.sqrt(width))
358
+ while ((width // col_len) * col_len) != width:
359
+ col_len -= 1
360
+ row_len = width // col_len
361
+
362
+ self._col_length, self._row_length = (
363
+ (row_len, col_len) if is_transpose else (col_len, row_len)
364
+ )
365
+
366
+ def _ct_pair_prob(self, q1, q2):
367
+ p1 = self.sim[q1[0]].prob(q1[1]) if isinstance(q1, tuple) else q1.prob()
368
+ p2 = self.sim[q2[0]].prob(q2[1]) if isinstance(q2, tuple) else q2.prob()
369
+
370
+ if p1 < p2:
371
+ return p2, q1
372
+
373
+ return p1, q2
374
+
375
+ def _cz_shadow(self, q1, q2):
376
+ prob_max, t = self._ct_pair_prob(q1, q2)
377
+ if prob_max > 0.5:
378
+ if isinstance(t, tuple):
379
+ self.sim[t[0]].z(t[1])
380
+ else:
381
+ t.z()
382
+
383
+ def _qec_x(self, c):
384
+ if isinstance(c, tuple):
385
+ self.sim[c[0]].x(c[1])
386
+ else:
387
+ c.x()
388
+
389
+ def _qec_h(self, t):
390
+ if isinstance(t, tuple):
391
+ self.sim[t[0]].h(t[1])
392
+ else:
393
+ t.h()
394
+
395
+ def _qec_s(self, t):
396
+ if isinstance(t, tuple):
397
+ self.sim[t[0]].s(t[1])
398
+ else:
399
+ t.s()
400
+
401
+ def _qec_adjs(self, t):
402
+ if isinstance(t, tuple):
403
+ self.sim[t[0]].adjs(t[1])
404
+ else:
405
+ t.adjs()
406
+
407
+ def _anti_cz_shadow(self, c, t):
408
+ self._qec_x(c)
409
+ self._cz_shadow(c, t)
410
+ self._qec_x(c)
411
+
412
+ def _cx_shadow(self, c, t):
413
+ self._qec_h(t)
414
+ self._cz_shadow(c, t)
415
+ self._qec_h(t)
416
+
417
+ def _anti_cx_shadow(self, c, t):
418
+ self._qec_x(c)
419
+ self._cx_shadow(c, t)
420
+ self._qec_x(c)
421
+
422
+ def _cy_shadow(self, c, t):
423
+ self._qec_adjs(t)
424
+ self._cx_shadow(c, t)
425
+ self._qec_s(t)
426
+
427
+ def _anti_cy_shadow(self, c, t):
428
+ self._qec_x(c)
429
+ self._cy_shadow(c, t)
430
+ self._qec_x(c)
431
+
432
+ def _unpack(self, lq):
433
+ return self._qubits[lq]
434
+
435
+ def _get_qb_lhv_indices(self, hq):
436
+ qb = []
437
+ if len(hq) < 2:
438
+ qb = [0]
439
+ lhv = -1
440
+ elif len(hq) < 4:
441
+ qb = [0, 1]
442
+ lhv = 2
443
+ else:
444
+ qb = [0, 1, 3, 4]
445
+ lhv = 2
446
+
447
+ return qb, lhv
448
+
449
+ def _get_lhv_bloch_angles(self, sim):
450
+ # Z axis
451
+ z = sim.bloch[2]
452
+
453
+ # X axis
454
+ x = sim.bloch[0]
455
+
456
+ # Y axis
457
+ y = sim.bloch[1]
458
+
459
+ inclination = math.atan2(math.sqrt(x**2 + y**2), z)
460
+ azimuth = math.atan2(y, x)
461
+
462
+ return azimuth, inclination
463
+
464
+ def _get_bloch_angles(self, hq):
465
+ sim = self.sim[hq[0]].clone()
466
+ q = hq[1]
467
+ sim.separate([q])
468
+
469
+ # Z axis
470
+ z = 1 - 2 * sim.prob(q)
471
+
472
+ # X axis
473
+ sim.h(q)
474
+ x = 1 - 2 * sim.prob(q)
475
+ sim.h(q)
476
+
477
+ # Y axis
478
+ sim.adjs(q)
479
+ sim.h(q)
480
+ y = 1 - 2 * sim.prob(q)
481
+ sim.h(q)
482
+ sim.s(q)
483
+
484
+ inclination = math.atan2(math.sqrt(x**2 + y**2), z)
485
+ azimuth = math.atan2(y, x)
486
+
487
+ return azimuth, inclination
488
+
489
+ def _rotate_to_bloch(self, hq, delta_azimuth, delta_inclination):
490
+ sim = self.sim[hq[0]]
491
+ q = hq[1]
492
+
493
+ # Apply rotation as "Azimuth, Inclination" (AI)
494
+ cosA = math.cos(delta_azimuth)
495
+ sinA = math.sin(delta_azimuth)
496
+ cosI = math.cos(delta_inclination / 2)
497
+ sinI = math.sin(delta_inclination / 2)
498
+
499
+ m00 = complex(cosI, 0)
500
+ m01 = complex(-cosA, sinA) * sinI
501
+ m10 = complex(cosA, sinA) * sinI
502
+ m11 = complex(cosI, 0)
503
+
504
+ sim.mtrx([m00, m01, m10, m11], q)
505
+
506
+ def _rotate_lhv_to_bloch(self, sim, delta_azimuth, delta_inclination):
507
+ # Apply rotation as "Azimuth, Inclination" (AI)
508
+ cosA = math.cos(delta_azimuth)
509
+ sinA = math.sin(delta_azimuth)
510
+ cosI = math.cos(delta_inclination / 2)
511
+ sinI = math.sin(delta_inclination / 2)
512
+
513
+ m00 = complex(cosI, 0)
514
+ m01 = complex(-cosA, sinA) * sinI
515
+ m10 = complex(cosA, sinA) * sinI
516
+ m11 = complex(cosI, 0)
517
+
518
+ sim.mtrx([m00, m01, m10, m11])
519
+
520
+ def _correct(self, lq, phase=False, skip_rotation=False):
521
+ hq = self._unpack(lq)
522
+
523
+ if len(hq) == 1:
524
+ return
525
+
526
+ qb, lhv = self._get_qb_lhv_indices(hq)
527
+
528
+ if phase:
529
+ for q in qb:
530
+ b = hq[q]
531
+ self.sim[b[0]].h(b[1])
532
+ b = hq[lhv]
533
+ b.h()
534
+
535
+ if len(hq) == 5:
536
+ # RMS
537
+ p = [
538
+ self.sim[hq[0][0]].prob(hq[0][1]),
539
+ self.sim[hq[1][0]].prob(hq[1][1]),
540
+ hq[2].prob(),
541
+ self.sim[hq[3][0]].prob(hq[3][1]),
542
+ self.sim[hq[4][0]].prob(hq[4][1]),
543
+ ]
544
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
545
+ prms = math.sqrt(
546
+ (p[0] ** 2 + p[1] ** 2 + 3 * (p[2] ** 2) + p[3] ** 2 + p[4] ** 2) / 7
547
+ )
548
+ qrms = math.sqrt(
549
+ (
550
+ (1 - p[0]) ** 2
551
+ + (1 - p[1]) ** 2
552
+ + 3 * ((1 - p[2]) ** 2)
553
+ + (1 - p[3]) ** 2
554
+ + (1 - p[4]) ** 2
555
+ )
556
+ / 7
557
+ )
558
+ result = ((prms + (1 - qrms)) / 2) >= 0.5
559
+ syndrome = (
560
+ [1 - p[0], 1 - p[1], 1 - p[2], 1 - p[3], 1 - p[4]]
561
+ if result
562
+ else [p[0], p[1], p[2], p[3], p[4]]
563
+ )
564
+ for q in range(5):
565
+ if syndrome[q] > (0.5 + self._epsilon):
566
+ if q == 2:
567
+ hq[q].x()
568
+ else:
569
+ self.sim[hq[q][0]].x(hq[q][1])
570
+
571
+ if not skip_rotation:
572
+ a, i = [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]
573
+ a[0], i[0] = self._get_bloch_angles(hq[0])
574
+ a[1], i[1] = self._get_bloch_angles(hq[1])
575
+ a[2], i[2] = self._get_lhv_bloch_angles(hq[2])
576
+ a[3], i[3] = self._get_bloch_angles(hq[3])
577
+ a[4], i[4] = self._get_bloch_angles(hq[4])
578
+
579
+ a_target = 0
580
+ i_target = 0
581
+ for x in range(5):
582
+ if x == 2:
583
+ continue
584
+ a_target += a[x]
585
+ i_target += i[x]
586
+
587
+ a_target /= 5
588
+ i_target /= 5
589
+ for x in range(5):
590
+ if x == 2:
591
+ self._rotate_lhv_to_bloch(
592
+ hq[x], a_target - a[x], i_target - i[x]
593
+ )
594
+ else:
595
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
596
+
597
+ else:
598
+ # RMS
599
+ p = [
600
+ self.sim[hq[0][0]].prob(hq[0][1]),
601
+ self.sim[hq[1][0]].prob(hq[1][1]),
602
+ hq[2].prob(),
603
+ ]
604
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
605
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
606
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
607
+ result = ((prms + (1 - qrms)) / 2) >= 0.5
608
+ syndrome = [1 - p[0], 1 - p[1], 1 - p[2]] if result else [p[0], p[1], p[2]]
609
+ for q in range(3):
610
+ if syndrome[q] > (0.5 + self._epsilon):
611
+ if q == 2:
612
+ hq[q].x()
613
+ else:
614
+ self.sim[hq[q][0]].x(hq[q][1])
615
+
616
+ if not skip_rotation:
617
+ a, i = [0, 0, 0], [0, 0, 0]
618
+ a[0], i[0] = self._get_bloch_angles(hq[0])
619
+ a[1], i[1] = self._get_bloch_angles(hq[1])
620
+ a[2], i[2] = self._get_lhv_bloch_angles(hq[2])
621
+
622
+ a_target = 0
623
+ i_target = 0
624
+ for x in range(3):
625
+ if x == 2:
626
+ continue
627
+ a_target += a[x]
628
+ i_target += i[x]
629
+
630
+ a_target /= 3
631
+ i_target /= 3
632
+ for x in range(3):
633
+ if x == 2:
634
+ self._rotate_lhv_to_bloch(
635
+ hq[x], a_target - a[x], i_target - i[x]
636
+ )
637
+ else:
638
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
639
+
640
+ if phase:
641
+ for q in qb:
642
+ b = hq[q]
643
+ self.sim[b[0]].h(b[1])
644
+ b = hq[lhv]
645
+ b.h()
646
+
647
+ def apply_magnetic_bias(self, q, b):
648
+ if b == 0:
649
+ return
650
+ b = math.exp(b)
651
+ for x in q:
652
+ hq = self._unpack(x)
653
+ for c in range(len(hq)):
654
+ h = hq[c]
655
+ if c == 2:
656
+ a, i = self._get_lhv_bloch_angles(h)
657
+ self._rotate_lhv_to_bloch(
658
+ h,
659
+ math.atan(math.tan(a) * b) - a,
660
+ math.atan(math.tan(i) * b) - i,
661
+ )
662
+ else:
663
+ a, i = self._get_bloch_angles(h)
664
+ self._rotate_to_bloch(
665
+ h,
666
+ math.atan(math.tan(a) * b) - a,
667
+ math.atan(math.tan(i) * b) - i,
668
+ )
669
+
670
+ def u(self, lq, th, ph, lm):
671
+ hq = self._unpack(lq)
672
+ if len(hq) < 2:
673
+ b = hq[0]
674
+ self.sim[b[0]].u(b[1], th, ph, lm)
675
+ return
676
+
677
+ qb, lhv = self._get_qb_lhv_indices(hq)
678
+
679
+ for q in qb:
680
+ b = hq[q]
681
+ self.sim[b[0]].u(b[1], th, ph, lm)
682
+
683
+ b = hq[lhv]
684
+ b.u(th, ph, lm)
685
+
686
+ self._correct(lq, False, True)
687
+ self._correct(lq, True, False)
688
+
689
+ def r(self, p, th, lq):
690
+ hq = self._unpack(lq)
691
+ if len(hq) < 2:
692
+ b = hq[0]
693
+ self.sim[b[0]].r(p, th, b[1])
694
+ return
695
+
696
+ qb, lhv = self._get_qb_lhv_indices(hq)
697
+
698
+ for q in qb:
699
+ b = hq[q]
700
+ self.sim[b[0]].r(p, th, b[1])
701
+
702
+ b = hq[lhv]
703
+ if p == Pauli.PauliX:
704
+ b.rx(th)
705
+ elif p == Pauli.PauliY:
706
+ b.ry(th)
707
+ elif p == Pauli.PauliZ:
708
+ b.rz(th)
709
+
710
+ if p != Pauli.PauliZ:
711
+ self._correct(lq, False, p != Pauli.PauliX)
712
+ if p != Pauli.PauliX:
713
+ self._correct(lq, True)
714
+
715
+ def h(self, lq):
716
+ hq = self._unpack(lq)
717
+ if len(hq) < 2:
718
+ b = hq[0]
719
+ self.sim[b[0]].h(b[1])
720
+ return
721
+
722
+ self._correct(lq)
723
+
724
+ qb, lhv = self._get_qb_lhv_indices(hq)
725
+
726
+ for q in qb:
727
+ b = hq[q]
728
+ self.sim[b[0]].h(b[1])
729
+
730
+ b = hq[lhv]
731
+ b.h()
732
+
733
+ self._correct(lq)
734
+
735
+ def s(self, lq):
736
+ hq = self._unpack(lq)
737
+ if len(hq) < 2:
738
+ b = hq[0]
739
+ self.sim[b[0]].s(b[1])
740
+ return
741
+
742
+ qb, lhv = self._get_qb_lhv_indices(hq)
743
+
744
+ for q in qb:
745
+ b = hq[q]
746
+ self.sim[b[0]].s(b[1])
747
+
748
+ b = hq[lhv]
749
+ b.s()
750
+
751
+ def adjs(self, lq):
752
+ hq = self._unpack(lq)
753
+ if len(hq) < 2:
754
+ b = hq[0]
755
+ self.sim[b[0]].adjs(b[1])
756
+ return
757
+
758
+ qb, lhv = self._get_qb_lhv_indices(hq)
759
+
760
+ for q in qb:
761
+ b = hq[q]
762
+ self.sim[b[0]].adjs(b[1])
763
+
764
+ b = hq[lhv]
765
+ b.adjs()
766
+
767
+ def x(self, lq):
768
+ hq = self._unpack(lq)
769
+ if len(hq) < 2:
770
+ b = hq[0]
771
+ self.sim[b[0]].x(b[1])
772
+ return
773
+
774
+ qb, lhv = self._get_qb_lhv_indices(hq)
775
+
776
+ for q in qb:
777
+ b = hq[q]
778
+ self.sim[b[0]].x(b[1])
779
+
780
+ b = hq[lhv]
781
+ b.x()
782
+
783
+ def y(self, lq):
784
+ hq = self._unpack(lq)
785
+ if len(hq) < 2:
786
+ b = hq[0]
787
+ self.sim[b[0]].y(b[1])
788
+ return
789
+
790
+ qb, lhv = self._get_qb_lhv_indices(hq)
791
+
792
+ for q in qb:
793
+ b = hq[q]
794
+ self.sim[b[0]].y(b[1])
795
+
796
+ b = hq[lhv]
797
+ b.y()
798
+
799
+ def z(self, lq):
800
+ hq = self._unpack(lq)
801
+ if len(hq) < 2:
802
+ b = hq[0]
803
+ self.sim[b[0]].z(b[1])
804
+ return
805
+
806
+ qb, lhv = self._get_qb_lhv_indices(hq)
807
+
808
+ for q in qb:
809
+ b = hq[q]
810
+ self.sim[b[0]].z(b[1])
811
+
812
+ b = hq[lhv]
813
+ b.z()
814
+
815
+ def t(self, lq):
816
+ hq = self._unpack(lq)
817
+ if len(hq) < 2:
818
+ b = hq[0]
819
+ self.sim[b[0]].t(b[1])
820
+ return
821
+
822
+ qb, lhv = self._get_qb_lhv_indices(hq)
823
+
824
+ for q in qb:
825
+ b = hq[q]
826
+ self.sim[b[0]].t(b[1])
827
+
828
+ b = hq[lhv]
829
+ b.t()
830
+
831
+ def adjt(self, lq):
832
+ hq = self._unpack(lq)
833
+ if len(hq) < 2:
834
+ b = hq[0]
835
+ self.sim[b[0]].adjt(b[1])
836
+ return
837
+
838
+ qb, lhv = self._get_qb_lhv_indices(hq)
839
+
840
+ for q in qb:
841
+ b = hq[q]
842
+ self.sim[b[0]].adjt(b[1])
843
+
844
+ b = hq[lhv]
845
+ b.adjt()
846
+
847
+ def _get_gate(self, pauli, anti, sim_id):
848
+ gate = None
849
+ shadow = None
850
+ if pauli == Pauli.PauliX:
851
+ gate = self.sim[sim_id].macx if anti else self.sim[sim_id].mcx
852
+ shadow = self._anti_cx_shadow if anti else self._cx_shadow
853
+ elif pauli == Pauli.PauliY:
854
+ gate = self.sim[sim_id].macy if anti else self.sim[sim_id].mcy
855
+ shadow = self._anti_cy_shadow if anti else self._cy_shadow
856
+ elif pauli == Pauli.PauliZ:
857
+ gate = self.sim[sim_id].macz if anti else self.sim[sim_id].mcz
858
+ shadow = self._anti_cz_shadow if anti else self._cz_shadow
859
+ else:
860
+ raise RuntimeError(
861
+ "QrackAceBackend._get_gate() should never return identity!"
862
+ )
863
+
864
+ return gate, shadow
865
+
866
+ def _get_connected(self, i, is_row):
867
+ long_range = self._is_row_long_range if is_row else self._is_col_long_range
868
+ length = self._col_length if is_row else self._row_length
869
+
870
+ connected = [i]
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
+ boundary = len(connected)
878
+ c = (i + 1) % length
879
+ while long_range[c] and (len(connected) < length):
880
+ connected.append(c)
881
+ c = (c + 1) % length
882
+ if len(connected) < length:
883
+ connected.append(c)
884
+
885
+ return connected, boundary
886
+
887
+ def _apply_coupling(self, pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr):
888
+ for q1 in qb1:
889
+ if q1 == lhv1:
890
+ continue
891
+ b1 = hq1[q1]
892
+ gate_fn, shadow_fn = self._get_gate(pauli, anti, b1[0])
893
+ for q2 in qb2:
894
+ if q2 == lhv2:
895
+ continue
896
+ b2 = hq2[q2]
897
+ if b1[0] == b2[0]:
898
+ gate_fn([b1[1]], b2[1])
899
+ elif (
900
+ lq1_lr
901
+ or (b1[1] == b2[1])
902
+ or ((len(qb1) == 2) and (b1[1] == (b2[1] & 1)))
903
+ ):
904
+ shadow_fn(b1, b2)
905
+
906
+ def _cpauli(self, lq1, lq2, anti, pauli):
907
+ lq1_row = lq1 // self._row_length
908
+ lq1_col = lq1 % self._row_length
909
+ lq2_row = lq2 // self._row_length
910
+ lq2_col = lq2 % self._row_length
911
+
912
+ hq1 = self._unpack(lq1)
913
+ hq2 = self._unpack(lq2)
914
+
915
+ lq1_lr = len(hq1) == 1
916
+ lq2_lr = len(hq2) == 1
917
+
918
+ self._correct(lq1)
919
+
920
+ qb1, lhv1 = self._get_qb_lhv_indices(hq1)
921
+ qb2, lhv2 = self._get_qb_lhv_indices(hq2)
922
+ # Apply cross coupling on hardware qubits first
923
+ self._apply_coupling(pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr)
924
+ # Apply coupling to the local-hidden-variable target
925
+ if lhv2 >= 0:
926
+ _cpauli_lhv(
927
+ hq1[lhv1].prob() if lhv1 >= 0 else self.sim[hq1[0][0]].prob(hq1[0][1]),
928
+ hq2[lhv2],
929
+ pauli,
930
+ anti,
931
+ )
932
+
933
+ self._correct(lq1, True)
934
+ if pauli != Pauli.PauliZ:
935
+ self._correct(lq2, False, pauli != Pauli.PauliX)
936
+ if pauli != Pauli.PauliX:
937
+ self._correct(lq2, True)
938
+
939
+ def cx(self, lq1, lq2):
940
+ self._cpauli(lq1, lq2, False, Pauli.PauliX)
941
+
942
+ def cy(self, lq1, lq2):
943
+ self._cpauli(lq1, lq2, False, Pauli.PauliY)
944
+
945
+ def cz(self, lq1, lq2):
946
+ self._cpauli(lq1, lq2, False, Pauli.PauliZ)
947
+
948
+ def acx(self, lq1, lq2):
949
+ self._cpauli(lq1, lq2, True, Pauli.PauliX)
950
+
951
+ def acy(self, lq1, lq2):
952
+ self._cpauli(lq1, lq2, True, Pauli.PauliY)
953
+
954
+ def acz(self, lq1, lq2):
955
+ self._cpauli(lq1, lq2, True, Pauli.PauliZ)
956
+
957
+ def mcx(self, lq1, lq2):
958
+ if len(lq1) > 1:
959
+ raise RuntimeError(
960
+ "QrackAceBackend.mcx() is provided for syntax convenience and only supports 1 control qubit!"
961
+ )
962
+ self._cpauli(lq1[0], lq2, False, Pauli.PauliX)
963
+
964
+ def mcy(self, lq1, lq2):
965
+ if len(lq1) > 1:
966
+ raise RuntimeError(
967
+ "QrackAceBackend.mcy() is provided for syntax convenience and only supports 1 control qubit!"
968
+ )
969
+ self._cpauli(lq1[0], lq2, False, Pauli.PauliY)
970
+
971
+ def mcz(self, lq1, lq2):
972
+ if len(lq1) > 1:
973
+ raise RuntimeError(
974
+ "QrackAceBackend.mcz() is provided for syntax convenience and only supports 1 control qubit!"
975
+ )
976
+ self._cpauli(lq1[0], lq2, False, Pauli.PauliZ)
977
+
978
+ def macx(self, lq1, lq2):
979
+ if len(lq1) > 1:
980
+ raise RuntimeError(
981
+ "QrackAceBackend.macx() is provided for syntax convenience and only supports 1 control qubit!"
982
+ )
983
+ self._cpauli(lq1[0], lq2, True, Pauli.PauliX)
984
+
985
+ def macy(self, lq1, lq2):
986
+ if len(lq1) > 1:
987
+ raise RuntimeError(
988
+ "QrackAceBackend.macy() is provided for syntax convenience and only supports 1 control qubit!"
989
+ )
990
+ self._cpauli(lq1[0], lq2, True, Pauli.PauliY)
991
+
992
+ def macz(self, lq1, lq2):
993
+ if len(lq1) > 1:
994
+ raise RuntimeError(
995
+ "QrackAceBackend.macz() is provided for syntax convenience and only supports 1 control qubit!"
996
+ )
997
+ self._cpauli(lq1[0], lq2, True, Pauli.PauliZ)
998
+
999
+ def swap(self, lq1, lq2):
1000
+ self.cx(lq1, lq2)
1001
+ self.cx(lq2, lq1)
1002
+ self.cx(lq1, lq2)
1003
+
1004
+ def iswap(self, lq1, lq2):
1005
+ self.swap(lq1, lq2)
1006
+ self.cz(lq1, lq2)
1007
+ self.s(lq1)
1008
+ self.s(lq2)
1009
+
1010
+ def adjiswap(self, lq1, lq2):
1011
+ self.adjs(lq2)
1012
+ self.adjs(lq1)
1013
+ self.cz(lq1, lq2)
1014
+ self.swap(lq1, lq2)
1015
+
1016
+ def prob(self, lq):
1017
+ hq = self._unpack(lq)
1018
+ if len(hq) < 2:
1019
+ b = hq[0]
1020
+ return self.sim[b[0]].prob(b[1])
1021
+
1022
+ self._correct(lq)
1023
+ if len(hq) == 5:
1024
+ # RMS
1025
+ p = [
1026
+ self.sim[hq[0][0]].prob(hq[0][1]),
1027
+ self.sim[hq[1][0]].prob(hq[1][1]),
1028
+ hq[2].prob(),
1029
+ self.sim[hq[3][0]].prob(hq[3][1]),
1030
+ self.sim[hq[4][0]].prob(hq[4][1]),
1031
+ ]
1032
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
1033
+ prms = math.sqrt(
1034
+ (p[0] ** 2 + p[1] ** 2 + 3 * (p[2] ** 2) + p[3] ** 2 + p[4] ** 2) / 7
1035
+ )
1036
+ qrms = math.sqrt(
1037
+ (
1038
+ (1 - p[0]) ** 2
1039
+ + (1 - p[1]) ** 2
1040
+ + 3 * ((1 - p[2]) ** 2)
1041
+ + (1 - p[3]) ** 2
1042
+ + (1 - p[4]) ** 2
1043
+ )
1044
+ / 7
1045
+ )
1046
+ else:
1047
+ # RMS
1048
+ p = [
1049
+ self.sim[hq[0][0]].prob(hq[0][1]),
1050
+ self.sim[hq[1][0]].prob(hq[1][1]),
1051
+ hq[2].prob(),
1052
+ ]
1053
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
1054
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
1055
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
1056
+
1057
+ return (prms + (1 - qrms)) / 2
1058
+
1059
+ def m(self, lq):
1060
+ hq = self._unpack(lq)
1061
+ if len(hq) < 2:
1062
+ b = hq[0]
1063
+ return self.sim[b[0]].m(b[1])
1064
+
1065
+ p = self.prob(lq)
1066
+ result = ((p + self._epsilon) >= 1) or (random.random() < p)
1067
+
1068
+ qb, lhv = self._get_qb_lhv_indices(hq)
1069
+
1070
+ for q in qb:
1071
+ b = hq[q]
1072
+ p = self.sim[b[0]].prob(b[1]) if result else (1 - self.sim[b[0]].prob(b[1]))
1073
+ if p < self._epsilon:
1074
+ if self.sim[b[0]].m(b[1]) != result:
1075
+ self.sim[b[0]].x(b[1])
1076
+ else:
1077
+ self.sim[b[0]].force_m(b[1], result)
1078
+
1079
+ b = hq[lhv]
1080
+ b.reset()
1081
+ if result:
1082
+ b.x()
1083
+
1084
+ return result
1085
+
1086
+ def force_m(self, lq, result):
1087
+ hq = self._unpack(lq)
1088
+ if len(hq) < 2:
1089
+ b = hq[0]
1090
+ return self.sim[b[0]].force_m(b[1], result)
1091
+
1092
+ self._correct(lq)
1093
+
1094
+ qb, lhv = self._get_qb_lhv_indices(hq)
1095
+
1096
+ for q in qb:
1097
+ b = hq[q]
1098
+ p = self.sim[b[0]].prob(b[1]) if result else (1 - self.sim[b[0]].prob(b[1]))
1099
+ if p < self._epsilon:
1100
+ if self.sim[b[0]].m(b[1]) != result:
1101
+ self.sim[b[0]].x(b[1])
1102
+ else:
1103
+ self.sim[b[0]].force_m(b[1], result)
1104
+
1105
+ b = hq[1]
1106
+ b.reset()
1107
+ if result:
1108
+ b.x()
1109
+
1110
+ return c
1111
+
1112
+ def m_all(self):
1113
+ # Randomize the order of measurement to amortize error.
1114
+ result = 0
1115
+ rows = list(range(self._col_length))
1116
+ random.shuffle(rows)
1117
+ for lq_row in rows:
1118
+ row_offset = lq_row * self._row_length
1119
+ cols = list(range(self._row_length))
1120
+ random.shuffle(cols)
1121
+ for lq_col in cols:
1122
+ lq = row_offset + lq_col
1123
+ if self.m(lq):
1124
+ result |= 1 << lq
1125
+
1126
+ return result
1127
+
1128
+ def measure_shots(self, q, s):
1129
+ samples = []
1130
+ for _ in range(s):
1131
+ clone = self.clone()
1132
+ _sample = clone.m_all()
1133
+ sample = 0
1134
+ for i in range(len(q)):
1135
+ if (_sample >> q[i]) & 1:
1136
+ sample |= 1 << i
1137
+ samples.append(sample)
1138
+
1139
+ return samples
1140
+
1141
+ def _apply_op(self, operation):
1142
+ name = operation.name
1143
+
1144
+ if (name == "id") or (name == "barrier"):
1145
+ # Skip measurement logic
1146
+ return
1147
+
1148
+ conditional = getattr(operation, "conditional", None)
1149
+ if isinstance(conditional, int):
1150
+ conditional_bit_set = (self._classical_register >> conditional) & 1
1151
+ if not conditional_bit_set:
1152
+ return
1153
+ elif conditional is not None:
1154
+ mask = int(conditional.mask, 16)
1155
+ if mask > 0:
1156
+ value = self._classical_memory & mask
1157
+ while (mask & 0x1) == 0:
1158
+ mask >>= 1
1159
+ value >>= 1
1160
+ if value != int(conditional.val, 16):
1161
+ return
1162
+
1163
+ if (name == "u1") or (name == "p"):
1164
+ self._sim.u(operation.qubits[0]._index, 0, 0, float(operation.params[0]))
1165
+ elif name == "u2":
1166
+ self._sim.u(
1167
+ operation.qubits[0]._index,
1168
+ math.pi / 2,
1169
+ float(operation.params[0]),
1170
+ float(operation.params[1]),
1171
+ )
1172
+ elif (name == "u3") or (name == "u"):
1173
+ self._sim.u(
1174
+ operation.qubits[0]._index,
1175
+ float(operation.params[0]),
1176
+ float(operation.params[1]),
1177
+ float(operation.params[2]),
1178
+ )
1179
+ elif name == "r":
1180
+ self._sim.u(
1181
+ operation.qubits[0]._index,
1182
+ float(operation.params[0]),
1183
+ float(operation.params[1]) - math.pi / 2,
1184
+ (-1 * float(operation.params[1])) + math.pi / 2,
1185
+ )
1186
+ elif name == "rx":
1187
+ self._sim.r(
1188
+ Pauli.PauliX, float(operation.params[0]), operation.qubits[0]._index
1189
+ )
1190
+ elif name == "ry":
1191
+ self._sim.r(
1192
+ Pauli.PauliY, float(operation.params[0]), operation.qubits[0]._index
1193
+ )
1194
+ elif name == "rz":
1195
+ self._sim.r(
1196
+ Pauli.PauliZ, float(operation.params[0]), operation.qubits[0]._index
1197
+ )
1198
+ elif name == "h":
1199
+ self._sim.h(operation.qubits[0]._index)
1200
+ elif name == "x":
1201
+ self._sim.x(operation.qubits[0]._index)
1202
+ elif name == "y":
1203
+ self._sim.y(operation.qubits[0]._index)
1204
+ elif name == "z":
1205
+ self._sim.z(operation.qubits[0]._index)
1206
+ elif name == "s":
1207
+ self._sim.s(operation.qubits[0]._index)
1208
+ elif name == "sdg":
1209
+ self._sim.adjs(operation.qubits[0]._index)
1210
+ elif name == "t":
1211
+ self._sim.t(operation.qubits[0]._index)
1212
+ elif name == "tdg":
1213
+ self._sim.adjt(operation.qubits[0]._index)
1214
+ elif name == "cx":
1215
+ self._sim.cx(operation.qubits[0]._index, operation.qubits[1]._index)
1216
+ elif name == "cy":
1217
+ self._sim.cy(operation.qubits[0]._index, operation.qubits[1]._index)
1218
+ elif name == "cz":
1219
+ self._sim.cz(operation.qubits[0]._index, operation.qubits[1]._index)
1220
+ elif name == "dcx":
1221
+ self._sim.mcx(operation.qubits[0]._index, operation.qubits[1]._index)
1222
+ self._sim.mcx(operation.qubits[1]._index, operation.qubits[0]._index)
1223
+ elif name == "swap":
1224
+ self._sim.swap(operation.qubits[0]._index, operation.qubits[1]._index)
1225
+ elif name == "iswap":
1226
+ self._sim.iswap(operation.qubits[0]._index, operation.qubits[1]._index)
1227
+ elif name == "iswap_dg":
1228
+ self._sim.adjiswap(operation.qubits[0]._index, operation.qubits[1]._index)
1229
+ elif name == "reset":
1230
+ qubits = operation.qubits
1231
+ for qubit in qubits:
1232
+ if self._sim.m(qubit._index):
1233
+ self._sim.x(qubit._index)
1234
+ elif name == "measure":
1235
+ qubits = operation.qubits
1236
+ clbits = operation.clbits
1237
+ cregbits = (
1238
+ operation.register
1239
+ if hasattr(operation, "register")
1240
+ else len(operation.qubits) * [-1]
1241
+ )
1242
+
1243
+ self._sample_qubits += qubits
1244
+ self._sample_clbits += clbits
1245
+ self._sample_cregbits += cregbits
1246
+
1247
+ if not self._sample_measure:
1248
+ for index in range(len(qubits)):
1249
+ qubit_outcome = self._sim.m(qubits[index]._index)
1250
+
1251
+ clbit = clbits[index]
1252
+ clmask = 1 << clbit
1253
+ self._classical_memory = (self._classical_memory & (~clmask)) | (
1254
+ qubit_outcome << clbit
1255
+ )
1256
+
1257
+ cregbit = cregbits[index]
1258
+ if cregbit < 0:
1259
+ cregbit = clbit
1260
+
1261
+ regbit = 1 << cregbit
1262
+ self._classical_register = (
1263
+ self._classical_register & (~regbit)
1264
+ ) | (qubit_outcome << cregbit)
1265
+
1266
+ elif name == "bfunc":
1267
+ mask = int(operation.mask, 16)
1268
+ relation = operation.relation
1269
+ val = int(operation.val, 16)
1270
+
1271
+ cregbit = operation.register
1272
+ cmembit = operation.memory if hasattr(operation, "memory") else None
1273
+
1274
+ compared = (self._classical_register & mask) - val
1275
+
1276
+ if relation == "==":
1277
+ outcome = compared == 0
1278
+ elif relation == "!=":
1279
+ outcome = compared != 0
1280
+ elif relation == "<":
1281
+ outcome = compared < 0
1282
+ elif relation == "<=":
1283
+ outcome = compared <= 0
1284
+ elif relation == ">":
1285
+ outcome = compared > 0
1286
+ elif relation == ">=":
1287
+ outcome = compared >= 0
1288
+ else:
1289
+ raise QrackError("Invalid boolean function relation.")
1290
+
1291
+ # Store outcome in register and optionally memory slot
1292
+ regbit = 1 << cregbit
1293
+ self._classical_register = (self._classical_register & (~regbit)) | (
1294
+ int(outcome) << cregbit
1295
+ )
1296
+ if cmembit is not None:
1297
+ membit = 1 << cmembit
1298
+ self._classical_memory = (self._classical_memory & (~membit)) | (
1299
+ int(outcome) << cmembit
1300
+ )
1301
+ else:
1302
+ err_msg = 'QrackAceBackend encountered unrecognized operation "{0}"'
1303
+ raise RuntimeError(err_msg.format(operation))
1304
+
1305
+ def _add_sample_measure(self, sample_qubits, sample_clbits, num_samples):
1306
+ """Generate data samples from current statevector.
1307
+
1308
+ Taken almost straight from the terra source code.
1309
+
1310
+ Args:
1311
+ measure_params (list): List of (qubit, clbit) values for
1312
+ measure instructions to sample.
1313
+ num_samples (int): The number of data samples to generate.
1314
+
1315
+ Returns:
1316
+ list: A list of data values in hex format.
1317
+ """
1318
+ # Get unique qubits that are actually measured
1319
+ measure_qubit = [qubit for qubit in sample_qubits]
1320
+ measure_clbit = [clbit for clbit in sample_clbits]
1321
+
1322
+ # Sample and convert to bit-strings
1323
+ if num_samples == 1:
1324
+ sample = self._sim.m_all()
1325
+ result = 0
1326
+ for index in range(len(measure_qubit)):
1327
+ qubit = measure_qubit[index]._index
1328
+ qubit_outcome = (sample >> qubit) & 1
1329
+ result |= qubit_outcome << index
1330
+ measure_results = [result]
1331
+ else:
1332
+ measure_results = self._sim.measure_shots(
1333
+ [q._index for q in measure_qubit], num_samples
1334
+ )
1335
+
1336
+ data = []
1337
+ for sample in measure_results:
1338
+ for index in range(len(measure_qubit)):
1339
+ qubit_outcome = (sample >> index) & 1
1340
+ clbit = measure_clbit[index]._index
1341
+ clmask = 1 << clbit
1342
+ self._classical_memory = (self._classical_memory & (~clmask)) | (
1343
+ qubit_outcome << clbit
1344
+ )
1345
+
1346
+ data.append(bin(self._classical_memory)[2:].zfill(self.num_qubits()))
1347
+
1348
+ return data
1349
+
1350
+ def run_qiskit_circuit(self, experiment, shots=1):
1351
+ if not _IS_QISKIT_AVAILABLE:
1352
+ raise RuntimeError(
1353
+ "Before trying to run_qiskit_circuit() with QrackAceBackend, you must install Qiskit!"
1354
+ )
1355
+
1356
+ instructions = []
1357
+ if isinstance(experiment, QuantumCircuit):
1358
+ instructions = experiment.data
1359
+ else:
1360
+ raise RuntimeError('Unrecognized "run_input" argument specified for run().')
1361
+
1362
+ self._shots = shots
1363
+ self._sample_qubits = []
1364
+ self._sample_clbits = []
1365
+ self._sample_cregbits = []
1366
+ self._sample_measure = True
1367
+ _data = []
1368
+ shotLoopMax = 1
1369
+
1370
+ is_initializing = True
1371
+ boundary_start = -1
1372
+
1373
+ for opcount in range(len(instructions)):
1374
+ operation = instructions[opcount]
1375
+
1376
+ if operation.name == "id" or operation.name == "barrier":
1377
+ continue
1378
+
1379
+ if is_initializing and (
1380
+ (operation.name == "measure") or (operation.name == "reset")
1381
+ ):
1382
+ continue
1383
+
1384
+ is_initializing = False
1385
+
1386
+ if (operation.name == "measure") or (operation.name == "reset"):
1387
+ if boundary_start == -1:
1388
+ boundary_start = opcount
1389
+
1390
+ if (boundary_start != -1) and (operation.name != "measure"):
1391
+ shotsPerLoop = 1
1392
+ shotLoopMax = self._shots
1393
+ self._sample_measure = False
1394
+ break
1395
+
1396
+ preamble_memory = 0
1397
+ preamble_register = 0
1398
+ preamble_sim = None
1399
+
1400
+ if self._sample_measure or boundary_start <= 0:
1401
+ boundary_start = 0
1402
+ self._sample_measure = True
1403
+ shotsPerLoop = self._shots
1404
+ shotLoopMax = 1
1405
+ else:
1406
+ boundary_start -= 1
1407
+ if boundary_start > 0:
1408
+ self._sim = self
1409
+ self._classical_memory = 0
1410
+ self._classical_register = 0
1411
+
1412
+ for operation in instructions[:boundary_start]:
1413
+ self._apply_op(operation)
1414
+
1415
+ preamble_memory = self._classical_memory
1416
+ preamble_register = self._classical_register
1417
+ preamble_sim = self._sim
1418
+
1419
+ for shot in range(shotLoopMax):
1420
+ if preamble_sim is None:
1421
+ self._sim = self
1422
+ self._classical_memory = 0
1423
+ self._classical_register = 0
1424
+ else:
1425
+ self._sim = QrackAceBackend(toClone=preamble_sim)
1426
+ self._classical_memory = preamble_memory
1427
+ self._classical_register = preamble_register
1428
+
1429
+ for operation in instructions[boundary_start:]:
1430
+ self._apply_op(operation)
1431
+
1432
+ if not self._sample_measure and (len(self._sample_qubits) > 0):
1433
+ _data += [bin(self._classical_memory)[2:].zfill(self.num_qubits())]
1434
+ self._sample_qubits = []
1435
+ self._sample_clbits = []
1436
+ self._sample_cregbits = []
1437
+
1438
+ if self._sample_measure and (len(self._sample_qubits) > 0):
1439
+ _data = self._add_sample_measure(
1440
+ self._sample_qubits, self._sample_clbits, self._shots
1441
+ )
1442
+
1443
+ del self._sim
1444
+
1445
+ return _data
1446
+
1447
+ def get_qiskit_basis_gates():
1448
+ return [
1449
+ "id",
1450
+ "u",
1451
+ "u1",
1452
+ "u2",
1453
+ "u3",
1454
+ "r",
1455
+ "rx",
1456
+ "ry",
1457
+ "rz",
1458
+ "h",
1459
+ "x",
1460
+ "y",
1461
+ "z",
1462
+ "s",
1463
+ "sdg",
1464
+ "sx",
1465
+ "sxdg",
1466
+ "p",
1467
+ "t",
1468
+ "tdg",
1469
+ "cx",
1470
+ "cy",
1471
+ "cz",
1472
+ "swap",
1473
+ "iswap",
1474
+ "reset",
1475
+ "measure",
1476
+ ]
1477
+
1478
+ # Mostly written by Dan, but with a little help from Elara (custom OpenAI GPT)
1479
+ def get_logical_coupling_map(self):
1480
+ if self._coupling_map:
1481
+ return self._coupling_map
1482
+
1483
+ coupling_map = set()
1484
+ rows, cols = self._row_length, self._col_length
1485
+
1486
+ # Map each column index to its full list of logical qubit indices
1487
+ def logical_index(row, col):
1488
+ return row * cols + col
1489
+
1490
+ for col in range(cols):
1491
+ connected_cols, _ = self._get_connected(col, False)
1492
+ for row in range(rows):
1493
+ connected_rows, _ = self._get_connected(row, False)
1494
+ a = logical_index(row, col)
1495
+ for c in connected_cols:
1496
+ for r in connected_rows:
1497
+ b = logical_index(r, c)
1498
+ if a != b:
1499
+ coupling_map.add((a, b))
1500
+
1501
+ self._coupling_map = sorted(coupling_map)
1502
+
1503
+ return self._coupling_map
1504
+
1505
+ # Designed by Dan, and implemented by Elara:
1506
+ def create_noise_model(self, x=0.25, y=0.25):
1507
+ if not _IS_QISKIT_AER_AVAILABLE:
1508
+ raise RuntimeError(
1509
+ "Before trying to run_qiskit_circuit() with QrackAceBackend, you must install Qiskit Aer!"
1510
+ )
1511
+ noise_model = NoiseModel()
1512
+
1513
+ for a, b in self.get_logical_coupling_map():
1514
+ col_a, col_b = a % self._row_length, b % self._row_length
1515
+ row_a, row_b = a // self._row_length, b // self._row_length
1516
+ is_long_a = self._is_col_long_range[col_a]
1517
+ is_long_b = self._is_col_long_range[col_b]
1518
+
1519
+ if is_long_a and is_long_b:
1520
+ continue # No noise on long-to-long
1521
+
1522
+ if (col_a == col_b) or (row_a == row_b):
1523
+ continue # No noise for same column
1524
+
1525
+ if is_long_a or is_long_b:
1526
+ y_cy = 1 - (1 - y) ** 2
1527
+ y_swap = 1 - (1 - y) ** 3
1528
+ noise_model.add_quantum_error(depolarizing_error(y, 2), "cx", [a, b])
1529
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cy", [a, b])
1530
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cz", [a, b])
1531
+ noise_model.add_quantum_error(
1532
+ depolarizing_error(y_swap, 2), "swap", [a, b]
1533
+ )
1534
+ else:
1535
+ y_cy = 1 - (1 - y) ** 2
1536
+ y_swap = 1 - (1 - y) ** 3
1537
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cx", [a, b])
1538
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cy", [a, b])
1539
+ noise_model.add_quantum_error(depolarizing_error(y_cy, 2), "cz", [a, b])
1540
+ noise_model.add_quantum_error(
1541
+ depolarizing_error(y_swap, 2), "swap", [a, b]
1542
+ )
1543
+
1544
+ return noise_model