pyqrack 1.44.33__py3-none-macosx_15_0_arm64.whl → 1.70.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.
@@ -3,6 +3,7 @@
3
3
  # Use of this source code is governed by an MIT-style license that can be
4
4
  # found in the LICENSE file or at https://opensource.org/licenses/MIT.
5
5
  import math
6
+ import os
6
7
  import random
7
8
  import sys
8
9
  import time
@@ -19,6 +20,175 @@ try:
19
20
  except ImportError:
20
21
  _IS_QISKIT_AVAILABLE = False
21
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
+
22
192
 
23
193
  class QrackAceBackend:
24
194
  """A back end for elided quantum error correction
@@ -28,45 +198,174 @@ class QrackAceBackend:
28
198
 
29
199
  The backend was originally designed assuming an (orbifolded) 2D qubit grid like 2019 Sycamore.
30
200
  However, it quickly became apparent that users can basically design their own connectivity topologies,
31
- without breaking the concept. (Not all will work equally well.) For maximum flexibility, set
32
- "alternating_codes=False". (For best performance on Sycamore-like topologies,leave it "True.")
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!)
33
206
 
34
207
  Attributes:
35
- sim(QrackSimulator): Corresponding simulator.
36
- alternating_codes(bool): Alternate repetition code elision by index?
37
- row_length(int): Qubits per row.
38
- col_length(int): Qubits per column.
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
39
211
  """
40
212
 
41
213
  def __init__(
42
214
  self,
43
215
  qubit_count=1,
44
- alternating_codes=True,
216
+ long_range_columns=4,
217
+ long_range_rows=4,
218
+ is_transpose=False,
45
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,
46
231
  toClone=None,
47
232
  ):
48
- self.sim = (
49
- toClone.sim.clone()
50
- if toClone
51
- else QrackSimulator(3 * qubit_count + 1, isTensorNetwork=isTensorNetwork)
52
- )
53
- self._ancilla = 3 * qubit_count
54
- self._factor_width(qubit_count)
55
- self.alternating_codes = alternating_codes
56
- self._is_init = [False] * qubit_count
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
+ )
57
303
 
58
- def _factor_width(self, width):
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):
59
357
  col_len = math.floor(math.sqrt(width))
60
358
  while ((width // col_len) * col_len) != width:
61
359
  col_len -= 1
62
360
  row_len = width // col_len
63
361
 
64
- self.col_length = col_len
65
- self.row_length = row_len
362
+ self._col_length, self._row_length = (
363
+ (row_len, col_len) if is_transpose else (col_len, row_len)
364
+ )
66
365
 
67
366
  def _ct_pair_prob(self, q1, q2):
68
- p1 = self.sim.prob(q1)
69
- p2 = self.sim.prob(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()
70
369
 
71
370
  if p1 < p2:
72
371
  return p2, q1
@@ -76,315 +375,566 @@ class QrackAceBackend:
76
375
  def _cz_shadow(self, q1, q2):
77
376
  prob_max, t = self._ct_pair_prob(q1, q2)
78
377
  if prob_max > 0.5:
79
- self.sim.z(t)
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()
80
388
 
81
- def _anti_cz_shadow(self, q1, q2):
82
- self.sim.x(q1)
83
- self._cz_shadow(q1, q2)
84
- self.sim.x(q1)
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)
85
411
 
86
412
  def _cx_shadow(self, c, t):
87
- self.sim.h(t)
413
+ self._qec_h(t)
88
414
  self._cz_shadow(c, t)
89
- self.sim.h(t)
415
+ self._qec_h(t)
90
416
 
91
417
  def _anti_cx_shadow(self, c, t):
92
- self.sim.x(t)
418
+ self._qec_x(c)
93
419
  self._cx_shadow(c, t)
94
- self.sim.x(t)
420
+ self._qec_x(c)
95
421
 
96
422
  def _cy_shadow(self, c, t):
97
- self.sim.adjs(t)
423
+ self._qec_adjs(t)
98
424
  self._cx_shadow(c, t)
99
- self.sim.s(t)
425
+ self._qec_s(t)
100
426
 
101
427
  def _anti_cy_shadow(self, c, t):
102
- self.sim.x(t)
428
+ self._qec_x(c)
103
429
  self._cy_shadow(c, t)
104
- self.sim.x(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
105
446
 
106
- def _unpack(self, lq, reverse=False):
107
- return (
108
- [3 * lq + 2, 3 * lq + 1, 3 * lq]
109
- if reverse
110
- else [3 * lq, 3 * lq + 1, 3 * lq + 2]
111
- )
447
+ return qb, lhv
112
448
 
113
- def _encode(self, hq, reverse=False):
114
- lq = hq[0] // 3
115
- row = lq // self.row_length
116
- even_row = not (row & 1)
117
- if ((not self.alternating_codes) and reverse) or (even_row == reverse):
118
- if self._is_init[lq]:
119
- # Encode shadow-first
120
- self._cx_shadow(hq[0], hq[1])
121
- self.sim.mcx([hq[1]], hq[2])
122
- else:
123
- self.sim.mcx([hq[2]], hq[1])
124
- else:
125
- if self._is_init[lq]:
126
- # Encode shadow-first
127
- self._cx_shadow(hq[0], hq[2])
128
- self.sim.mcx([hq[0]], hq[1])
129
- self._is_init[lq] = True
130
-
131
- def _decode(self, hq, reverse=False):
132
- lq = hq[0] // 3
133
- if not self._is_init[lq]:
134
- return
135
- row = lq // self.row_length
136
- even_row = not (row & 1)
137
- if ((not self.alternating_codes) and reverse) or (even_row == reverse):
138
- # Decode entangled-first
139
- self.sim.mcx([hq[1]], hq[2])
140
- self._cx_shadow(hq[0], hq[1])
141
- else:
142
- # Decode entangled-first
143
- self.sim.mcx([hq[0]], hq[1])
144
- self._cx_shadow(hq[0], hq[2])
449
+ def _get_lhv_bloch_angles(self, sim):
450
+ # Z axis
451
+ z = sim.bloch[2]
145
452
 
146
- def _correct(self, lq):
147
- if not self._is_init[lq]:
148
- return
149
- # We can't use true syndrome-based error correction,
150
- # because one of the qubits in the code is separated.
151
- # However, we can get pretty close!
152
- shots = 1024
153
- even_row = not ((lq // self.row_length) & 1)
154
- single_bit = 0
155
- other_bits = []
156
- if not self.alternating_codes or even_row:
157
- single_bit = 2
158
- other_bits = [0, 1]
159
- else:
160
- single_bit = 0
161
- other_bits = [1, 2]
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):
162
521
  hq = self._unpack(lq)
163
- single_bit_value = self.sim.prob(hq[single_bit])
164
- single_bit_polarization = max(single_bit_value, 1 - single_bit_value)
165
- samples = self.sim.measure_shots([hq[other_bits[0]], hq[other_bits[1]]], shots)
166
- syndrome_indices = (
167
- [other_bits[1], other_bits[0]]
168
- if (single_bit_value >= 0.5)
169
- else [other_bits[0], other_bits[1]]
170
- )
171
- syndrome = [0, 0, 0]
172
- values = []
173
- for sample in samples:
174
- match sample:
175
- case 0:
176
- value = single_bit_value
177
- syndrome[single_bit] += value
178
- case 1:
179
- value = single_bit_polarization
180
- syndrome[syndrome_indices[0]] += value
181
- case 2:
182
- value = single_bit_polarization
183
- syndrome[syndrome_indices[1]] += value
184
- case 3:
185
- value = 1 - single_bit_value
186
- syndrome[single_bit] += value
187
- values.append(value)
188
-
189
- # Suggestion from Elara (custom OpenAI GPT):
190
- # Compute the standard deviation and only correct if we're outside a confidence interval.
191
- # (This helps avoid limit-point over-correction.)
192
- syndrome_sum = sum(syndrome)
193
- z_score_numer = syndrome_sum - shots / 2
194
- z_score = 0
195
- if z_score_numer > 0:
196
- syndrome_component_mean = syndrome_sum / shots
197
- syndrome_total_variance = sum(
198
- (value - syndrome_component_mean) ** 2 for value in values
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
199
547
  )
200
- z_score_denom = math.sqrt(syndrome_total_variance)
201
- z_score = (
202
- math.inf
203
- if math.isclose(z_score_denom, 0)
204
- else (z_score_numer / z_score_denom)
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]]
205
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])
206
596
 
207
- force_syndrome = True
208
- # (From Elara, this is the value that minimizes the sum of Type I and Type II error.)
209
- if z_score >= (497 / 999):
210
- # There is an error.
211
- error_bit = syndrome.index(max(syndrome))
212
- if error_bit == single_bit:
213
- # The stand-alone bit carries the error.
214
- self.sim.x(hq[error_bit])
215
- else:
216
- # The coherent bits carry the error.
217
- force_syndrome = False
218
- # Form their syndrome.
219
- self.sim.mcx([hq[other_bits[0]]], self._ancilla)
220
- self.sim.mcx([hq[other_bits[1]]], self._ancilla)
221
- # Force the syndrome pathological
222
- self.sim.force_m(self._ancilla, True)
223
- # Reset the ancilla.
224
- self.sim.x(self._ancilla)
225
- # Correct the bit flip.
226
- self.sim.x(hq[error_bit])
227
-
228
- # There is no error.
229
- if force_syndrome:
230
- # Form the syndrome of the coherent bits.
231
- self.sim.mcx([hq[other_bits[0]]], self._ancilla)
232
- self.sim.mcx([hq[other_bits[1]]], self._ancilla)
233
- # Force the syndrome non-pathological.
234
- self.sim.force_m(self._ancilla, False)
235
-
236
- def _correct_if_like_h(self, th, lq):
237
- if not self._is_init[lq]:
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:
238
649
  return
239
- while th > math.pi:
240
- th -= 2 * math.pi
241
- while th <= -math.pi:
242
- th += 2 * math.pi
243
- th = abs(th)
244
- if not math.isclose(th, 0):
245
- self._correct(lq)
246
-
247
- def u(self, th, ph, lm, lq):
248
- while ph > math.pi:
249
- ph -= 2 * math.pi
250
- while ph <= -math.pi:
251
- ph += 2 * math.pi
252
- while lm > math.pi:
253
- lm -= 2 * math.pi
254
- while lm <= -math.pi:
255
- lm += 2 * math.pi
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):
256
671
  hq = self._unpack(lq)
257
- if not math.isclose(ph, -lm) and not math.isclose(abs(ph), math.pi / 2):
258
- self._correct_if_like_h(th, lq)
259
- self._decode(hq)
260
- self.sim.u(hq[0], th, ph, lm)
261
- if not self._is_init[lq]:
262
- self.sim.u(hq[2], th, ph, lm)
263
- self._encode(hq)
264
- else:
265
- for b in hq:
266
- self.sim.u(b, th, ph, lm)
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)
267
688
 
268
689
  def r(self, p, th, lq):
269
- while th > math.pi:
270
- th -= 2 * math.pi
271
- while th <= -math.pi:
272
- th += 2 * math.pi
273
- if p == Pauli.PauliY:
274
- self._correct_if_like_h(th, lq)
275
690
  hq = self._unpack(lq)
276
- if (p == Pauli.PauliZ) or math.isclose(abs(th), math.pi):
277
- for b in hq:
278
- self.sim.r(p, th, b)
279
- else:
280
- self._decode(hq)
281
- self.sim.r(p, th, hq[0])
282
- if not self._is_init[lq]:
283
- self.sim.r(p, th, hq[2])
284
- self._encode(hq)
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)
285
714
 
286
715
  def h(self, lq):
287
716
  hq = self._unpack(lq)
288
- self._decode(hq)
289
- self.sim.h(hq[0])
290
- if not self._is_init[lq]:
291
- self.sim.h(hq[2])
292
- self._encode(hq)
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)
293
734
 
294
735
  def s(self, lq):
295
736
  hq = self._unpack(lq)
296
- for b in hq:
297
- self.sim.s(b)
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()
298
750
 
299
751
  def adjs(self, lq):
300
752
  hq = self._unpack(lq)
301
- for b in hq:
302
- self.sim.adjs(b)
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()
303
766
 
304
767
  def x(self, lq):
305
768
  hq = self._unpack(lq)
306
- for b in hq:
307
- self.sim.x(b)
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()
308
782
 
309
783
  def y(self, lq):
310
784
  hq = self._unpack(lq)
311
- for b in hq:
312
- self.sim.y(b)
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()
313
798
 
314
799
  def z(self, lq):
315
800
  hq = self._unpack(lq)
316
- for b in hq:
317
- self.sim.z(b)
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()
318
814
 
319
815
  def t(self, lq):
320
816
  hq = self._unpack(lq)
321
- for b in hq:
322
- self.sim.t(b)
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()
323
830
 
324
831
  def adjt(self, lq):
325
832
  hq = self._unpack(lq)
326
- for b in hq:
327
- self.sim.adjt(b)
833
+ if len(hq) < 2:
834
+ b = hq[0]
835
+ self.sim[b[0]].adjt(b[1])
836
+ return
328
837
 
329
- def _cpauli(self, lq1, lq2, anti, pauli):
330
- self._correct(lq1)
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):
331
848
  gate = None
332
849
  shadow = None
333
850
  if pauli == Pauli.PauliX:
334
- gate = self.sim.macx if anti else self.sim.mcx
851
+ gate = self.sim[sim_id].macx if anti else self.sim[sim_id].mcx
335
852
  shadow = self._anti_cx_shadow if anti else self._cx_shadow
336
853
  elif pauli == Pauli.PauliY:
337
- gate = self.sim.macy if anti else self.sim.mcy
854
+ gate = self.sim[sim_id].macy if anti else self.sim[sim_id].mcy
338
855
  shadow = self._anti_cy_shadow if anti else self._cy_shadow
339
856
  elif pauli == Pauli.PauliZ:
340
- gate = self.sim.macz if anti else self.sim.mcz
857
+ gate = self.sim[sim_id].macz if anti else self.sim[sim_id].mcz
341
858
  shadow = self._anti_cz_shadow if anti else self._cz_shadow
342
859
  else:
343
- return
860
+ raise RuntimeError(
861
+ "QrackAceBackend._get_gate() should never return identity!"
862
+ )
344
863
 
345
- if not self._is_init[lq1]:
346
- hq1 = self._unpack(lq1)
347
- hq2 = self._unpack(lq2)
348
- gate([hq1[0]], hq2[0])
349
- gate([hq1[1]], hq2[1])
350
- gate([hq1[2]], hq2[2])
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)
351
905
 
352
- return
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
353
911
 
354
- lq1_col = lq1 // self.row_length
355
- lq1_row = lq1 % self.row_length
356
- lq2_col = lq2 // self.row_length
357
- lq2_row = lq2 % self.row_length
358
-
359
- hq1 = None
360
- hq2 = None
361
- if (lq2_col == lq1_col) and (((lq1_row + 1) % self.row_length) == lq2_row):
362
- self._correct(lq2)
363
- hq1 = self._unpack(lq1, True)
364
- hq2 = self._unpack(lq2, False)
365
- self._decode(hq1, True)
366
- self._decode(hq2, False)
367
- gate([hq1[0]], hq2[0])
368
- self._encode(hq2, False)
369
- self._encode(hq1, True)
370
- elif (lq1_col == lq2_col) and (((lq2_row + 1) % self.row_length) == lq1_row):
371
- self._correct(lq2)
372
- hq2 = self._unpack(lq2, True)
373
- hq1 = self._unpack(lq1, False)
374
- self._decode(hq2, True)
375
- self._decode(hq1, False)
376
- gate([hq1[0]], hq2[0])
377
- self._encode(hq1, False)
378
- self._encode(hq2, True)
379
- else:
380
- hq1 = self._unpack(lq1)
381
- hq2 = self._unpack(lq2)
382
- gate([hq1[0]], hq2[0])
383
- if self.alternating_codes and ((lq2_col & 1) != (lq1_col & 1)):
384
- shadow(hq1[1], hq2[1])
385
- else:
386
- gate([hq1[1]], hq2[1])
387
- gate([hq1[2]], hq2[2])
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)
388
938
 
389
939
  def cx(self, lq1, lq2):
390
940
  self._cpauli(lq1, lq2, False, Pauli.PauliX)
@@ -404,6 +954,48 @@ class QrackAceBackend:
404
954
  def acz(self, lq1, lq2):
405
955
  self._cpauli(lq1, lq2, True, Pauli.PauliZ)
406
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
+
407
999
  def swap(self, lq1, lq2):
408
1000
  self.cx(lq1, lq2)
409
1001
  self.cx(lq2, lq1)
@@ -421,85 +1013,130 @@ class QrackAceBackend:
421
1013
  self.cz(lq1, lq2)
422
1014
  self.swap(lq1, lq2)
423
1015
 
424
- def m(self, lq):
1016
+ def prob(self, lq):
425
1017
  hq = self._unpack(lq)
426
- even_row = not ((lq // self.row_length) & 1)
427
- if not self.alternating_codes or even_row:
428
- single_bit = 2
429
- other_bits = [0, 1]
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
+ )
430
1046
  else:
431
- single_bit = 0
432
- other_bits = [1, 2]
433
- syndrome = 0
434
- bits = []
435
- for q in other_bits:
436
- bits.append(self.sim.m(hq[q]))
437
- if bits[-1]:
438
- syndrome += 1
439
- # single_bit never shares entanglement with other_bits.
440
- # In the ideal, it should simply duplicate other_bits.
441
- # So get more precision by using it analytically.
442
- analytical = self.sim.prob(hq[single_bit])
443
- syndrome += analytical
444
- result = True if (syndrome >= 1.5) else False
445
- # The two separable parts of the code are correlated,
446
- # but not non-locally, via entanglement.
447
- # Prefer to collapse the analytical part toward agreement.
448
- bits.append(self.sim.m(hq[single_bit]) if math.isclose(analytical, 0 if result else 1) else self.sim.force_m(hq[single_bit], result))
449
- for i in range(2):
450
- if bits[i] != result:
451
- self.sim.x(hq[other_bits[i]])
452
- if bits[2] != result:
453
- self.sim.x(hq[single_bit])
454
- self._is_init[lq] = False
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()
455
1083
 
456
1084
  return result
457
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
+
458
1112
  def m_all(self):
1113
+ # Randomize the order of measurement to amortize error.
459
1114
  result = 0
460
- for lq in range(self.sim.num_qubits() // 3):
461
- result <<= 1
462
- if self.m(lq):
463
- result |= 1
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
464
1125
 
465
1126
  return result
466
1127
 
467
- def measure_shots(self, q, s, high_accuracy=False):
468
- if high_accuracy:
469
- samples = []
470
- for _ in range(s):
471
- clone = self.sim.clone()
472
- sample = 0
473
- for i in range(len(q)):
474
- if clone.m(q[i]):
475
- sample |= 1 << i
476
- samples.append(sample)
477
-
478
- return samples
479
-
480
- _q = []
481
- for i in q:
482
- _q.append(3 * i)
483
- _q.append(3 * i + 1)
484
- _q.append(3 * i + 2)
485
-
486
- samples = self.sim.measure_shots(_q, s)
487
-
488
- results = []
489
- for sample in samples:
490
- logical_sample = 0
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
491
1134
  for i in range(len(q)):
492
- logical_sample <<= 1
493
- bit_count = 0
494
- for _ in range(3):
495
- if sample & 1:
496
- bit_count += 1
497
- sample >>= 1
498
- if bit_count > 1:
499
- logical_sample |= 1
500
- results.append(logical_sample)
501
-
502
- return results
1135
+ if (_sample >> q[i]) & 1:
1136
+ sample |= 1 << i
1137
+ samples.append(sample)
1138
+
1139
+ return samples
503
1140
 
504
1141
  def _apply_op(self, operation):
505
1142
  name = operation.name
@@ -524,27 +1161,27 @@ class QrackAceBackend:
524
1161
  return
525
1162
 
526
1163
  if (name == "u1") or (name == "p"):
527
- self._sim.u(0, 0, float(operation.params[0]), operation.qubits[0]._index)
1164
+ self._sim.u(operation.qubits[0]._index, 0, 0, float(operation.params[0]))
528
1165
  elif name == "u2":
529
1166
  self._sim.u(
1167
+ operation.qubits[0]._index,
530
1168
  math.pi / 2,
531
1169
  float(operation.params[0]),
532
1170
  float(operation.params[1]),
533
- operation.qubits[0]._index,
534
1171
  )
535
1172
  elif (name == "u3") or (name == "u"):
536
1173
  self._sim.u(
1174
+ operation.qubits[0]._index,
537
1175
  float(operation.params[0]),
538
1176
  float(operation.params[1]),
539
1177
  float(operation.params[2]),
540
- operation.qubits[0]._index,
541
1178
  )
542
1179
  elif name == "r":
543
1180
  self._sim.u(
1181
+ operation.qubits[0]._index,
544
1182
  float(operation.params[0]),
545
1183
  float(operation.params[1]) - math.pi / 2,
546
1184
  (-1 * float(operation.params[1])) + math.pi / 2,
547
- operation.qubits[0]._index,
548
1185
  )
549
1186
  elif name == "rx":
550
1187
  self._sim.r(
@@ -806,3 +1443,102 @@ class QrackAceBackend:
806
1443
  del self._sim
807
1444
 
808
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