pyqrack-cuda-complex128 2.2.1__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {pyqrack_cuda_complex128-2.2.1/pyqrack_cuda_complex128.egg-info → pyqrack_cuda_complex128-2.2.2}/PKG-INFO +1 -1
  2. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_ace_backend.py +380 -104
  3. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2/pyqrack_cuda_complex128.egg-info}/PKG-INFO +1 -1
  4. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/setup.py +1 -1
  5. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/LICENSE +0 -0
  6. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/MANIFEST.in +0 -0
  7. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/Makefile +0 -0
  8. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/README.md +0 -0
  9. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyproject.toml +0 -0
  10. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/__init__.py +0 -0
  11. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/neuron_activation_fn.py +0 -0
  12. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/pauli.py +0 -0
  13. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_circuit.py +0 -0
  14. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  15. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_neuron.py +0 -0
  16. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  17. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_simulator.py +0 -0
  18. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_stabilizer.py +0 -0
  19. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_system/__init__.py +0 -0
  20. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/qrack_system/qrack_system.py +0 -0
  21. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/quimb_circuit_type.py +0 -0
  22. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/stats/__init__.py +0 -0
  23. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/stats/load_quantized_data.py +0 -0
  24. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack/stats/quantize_by_range.py +0 -0
  25. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack_cuda_complex128.egg-info/SOURCES.txt +0 -0
  26. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack_cuda_complex128.egg-info/dependency_links.txt +0 -0
  27. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack_cuda_complex128.egg-info/not-zip-safe +0 -0
  28. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack_cuda_complex128.egg-info/requires.txt +0 -0
  29. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/pyqrack_cuda_complex128.egg-info/top_level.txt +0 -0
  30. {pyqrack_cuda_complex128-2.2.1 → pyqrack_cuda_complex128-2.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cuda-complex128
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -29,6 +29,169 @@ except ImportError:
29
29
  _IS_QISKIT_AER_AVAILABLE = False
30
30
 
31
31
 
32
+ # Initial stub and concept produced through conversation with Elara
33
+ # (the custom OpenAI GPT)
34
+ class LHVQubit:
35
+ def __init__(self, to_clone=None):
36
+ # Initial state in "Bloch vector" terms, defaults to |0⟩
37
+ if to_clone:
38
+ self.bloch = to_clone.bloch.copy()
39
+ else:
40
+ self.reset()
41
+
42
+ def reset(self):
43
+ self.bloch = [0.0, 0.0, 1.0]
44
+
45
+ def h(self):
46
+ # Hadamard: rotate around Y-axis then X-axis (simplified for LHV)
47
+ x, y, z = self.bloch
48
+ self.bloch = [(x + z) / math.sqrt(2), y, (z - x) / math.sqrt(2)]
49
+
50
+ def x(self):
51
+ x, y, z = self.bloch
52
+ self.bloch = [x, y, -z]
53
+
54
+ def y(self):
55
+ x, y, z = self.bloch
56
+ self.bloch = [-x, y, z]
57
+
58
+ def z(self):
59
+ x, y, z = self.bloch
60
+ self.bloch = [x, -y, z]
61
+
62
+ def rx(self, theta):
63
+ # Rotate Bloch vector around X-axis by angle theta
64
+ x, y, z = self.bloch
65
+ cos_theta = math.cos(theta)
66
+ sin_theta = math.sin(theta)
67
+ new_y = cos_theta * y - sin_theta * z
68
+ new_z = sin_theta * y + cos_theta * z
69
+ self.bloch = [x, new_y, new_z]
70
+
71
+ def ry(self, theta):
72
+ # Rotate Bloch vector around Y-axis by angle theta
73
+ x, y, z = self.bloch
74
+ cos_theta = math.cos(theta)
75
+ sin_theta = math.sin(theta)
76
+ new_x = cos_theta * x + sin_theta * z
77
+ new_z = -sin_theta * x + cos_theta * z
78
+ self.bloch = [new_x, y, new_z]
79
+
80
+ def rz(self, theta):
81
+ # Rotate Bloch vector around Z-axis by angle theta (in radians)
82
+ x, y, z = self.bloch
83
+ cos_theta = math.cos(theta)
84
+ sin_theta = math.sin(theta)
85
+ new_x = cos_theta * x - sin_theta * y
86
+ new_y = sin_theta * x + cos_theta * y
87
+ self.bloch = [new_x, new_y, z]
88
+
89
+ def s(self):
90
+ self.rz(math.pi / 2)
91
+
92
+ def adjs(self):
93
+ self.rz(-math.pi / 2)
94
+
95
+ def t(self):
96
+ self.rz(math.pi / 4)
97
+
98
+ def adjt(self):
99
+ self.rz(-math.pi / 4)
100
+
101
+ def u(self, theta, phi, lam):
102
+ # Apply general single-qubit unitary gate
103
+ self.rz(lam)
104
+ self.ry(theta)
105
+ self.rz(phi)
106
+
107
+ # Provided verbatim by Elara (the custom OpenAI GPT):
108
+ def mtrx(self, matrix):
109
+ """
110
+ Apply a 2x2 unitary matrix to the LHV Bloch vector using only standard math/cmath.
111
+ Matrix format: [a, b, c, d] for [[a, b], [c, d]]
112
+ """
113
+ a, b, c, d = matrix
114
+
115
+ # Current Bloch vector
116
+ x, y, z = self.bloch
117
+
118
+ # Convert to density matrix ρ = ½ (I + xσx + yσy + zσz)
119
+ rho = [[(1 + z) / 2, (x - 1j * y) / 2], [(x + 1j * y) / 2, (1 - z) / 2]]
120
+
121
+ # Compute U * ρ
122
+ u_rho = [
123
+ [a * rho[0][0] + b * rho[1][0], a * rho[0][1] + b * rho[1][1]],
124
+ [c * rho[0][0] + d * rho[1][0], c * rho[0][1] + d * rho[1][1]],
125
+ ]
126
+
127
+ # Compute (U * ρ) * U†
128
+ rho_prime = [
129
+ [
130
+ u_rho[0][0] * a.conjugate() + u_rho[0][1] * b.conjugate(),
131
+ u_rho[0][0] * c.conjugate() + u_rho[0][1] * d.conjugate(),
132
+ ],
133
+ [
134
+ u_rho[1][0] * a.conjugate() + u_rho[1][1] * b.conjugate(),
135
+ u_rho[1][0] * c.conjugate() + u_rho[1][1] * d.conjugate(),
136
+ ],
137
+ ]
138
+
139
+ # Extract Bloch components: Tr(ρ'σi) = 2 * Re[...]
140
+ new_x = 2 * rho_prime[0][1].real + 2 * rho_prime[1][0].real
141
+ new_y = 2 * (rho_prime[0][1].imag - rho_prime[1][0].imag)
142
+ new_z = 2 * rho_prime[0][0].real - 1 # since Tr(ρ') = 1
143
+
144
+ p = math.sqrt(new_x**2 + new_y**2 + new_z**2)
145
+
146
+ new_x /= p
147
+ new_y /= p
148
+ new_z /= p
149
+
150
+ self.bloch = [new_x, new_y, new_z]
151
+
152
+ def prob(self, basis=Pauli.PauliZ):
153
+ """Sample a classical outcome from the current 'quantum' state"""
154
+ if basis == Pauli.PauliZ:
155
+ prob_1 = (1 - self.bloch[2]) / 2
156
+ elif basis == Pauli.PauliX:
157
+ prob_1 = (1 - self.bloch[0]) / 2
158
+ elif basis == Pauli.PauliY:
159
+ prob_1 = (1 - self.bloch[1]) / 2
160
+ else:
161
+ raise ValueError(f"Unsupported basis: {basis}")
162
+ return prob_1
163
+
164
+ def m(self):
165
+ result = random.random() < self.prob()
166
+ self.reset()
167
+ if result:
168
+ self.x()
169
+ return result
170
+
171
+
172
+ # Provided by Elara (the custom OpenAI GPT)
173
+ def _cpauli_lhv(prob, targ, axis, anti, theta=math.pi):
174
+ """
175
+ Apply a 'soft' controlled-Pauli gate: rotate target qubit
176
+ proportionally to control's Z expectation value.
177
+
178
+ theta: full rotation angle if control in |1⟩
179
+ """
180
+ # Control influence is (1 - ctrl.bloch[2]) / 2 = P(|1⟩)
181
+ # BUT we avoid collapse by using the expectation value:
182
+ control_influence = (1 - prob) if anti else prob
183
+
184
+ effective_theta = control_influence * theta
185
+
186
+ # Apply partial rotation to target qubit:
187
+ if axis == Pauli.PauliX:
188
+ targ.rx(effective_theta)
189
+ elif axis == Pauli.PauliY:
190
+ targ.ry(effective_theta)
191
+ elif axis == Pauli.PauliZ:
192
+ targ.rz(effective_theta)
193
+
194
+
32
195
  class QrackAceBackend:
33
196
  """A back end for elided quantum error correction
34
197
 
@@ -113,18 +276,6 @@ class QrackAceBackend:
113
276
  self._is_row_long_range[-1] = False
114
277
  sim_count = col_patch_count * row_patch_count
115
278
 
116
- # Boundary qubits no longer carry a private classical LHV proxy.
117
- # Instead, every boundary site (row- and/or column-boundary alike)
118
- # gets a real qubit in one single, shared "crossbar" QrackSimulator.
119
- # That simulator's own greedy elision (set_sdrp) is trusted to
120
- # automatically factor apart whatever boundary sites turn out to be
121
- # separable (e.g. disjoint rails, rail intersections), exactly the
122
- # same way it already factors apart unentangled subspaces within
123
- # any other single QrackSimulator instance. We don't need to special
124
- # -case "crossbar intersections" by hand; the elision does it for us.
125
- boundary_sim_id = sim_count
126
- boundary_count = 0
127
-
128
279
  self._qubits = []
129
280
  sim_counts = [0] * sim_count
130
281
  sim_id = 0
@@ -139,8 +290,9 @@ class QrackAceBackend:
139
290
  qubit.append((t_sim_id, sim_counts[t_sim_id]))
140
291
  sim_counts[t_sim_id] += 1
141
292
 
142
- qubit.append((boundary_sim_id, boundary_count))
143
- boundary_count += 1
293
+ qubit.append(
294
+ LHVQubit(to_clone=(to_clone._qubits[tot_qubits][2] if to_clone else None))
295
+ )
144
296
 
145
297
  if (not c) and (not r):
146
298
  t_sim_id = (sim_id + col_patch_count) % sim_count
@@ -157,23 +309,8 @@ class QrackAceBackend:
157
309
  self._qubits.append(qubit)
158
310
  tot_qubits += 1
159
311
 
160
- # The crossbar's size is fixed by how many boundary sites exist.
161
- # When there are none (e.g. a grid small enough, relative to
162
- # long_range_rows/columns, that the whole thing is "fully
163
- # connected" with no QEC boundary at all), we must NOT allocate a
164
- # 0-qubit QrackSimulator for the crossbar: a 0-qubit QrackSimulator
165
- # can be constructed, but calling .clone() on one crashes the
166
- # native Qrack core (segfault), and clone() is called routinely by
167
- # measure_shots()/clone(). So the boundary sim is only created when
168
- # boundary_count > 0, exactly mirroring how the original LHV-based
169
- # code never instantiated anything for the boundary case when
170
- # there were no boundary sites.
171
- has_boundary = boundary_count > 0
172
- if has_boundary:
173
- sim_counts.append(boundary_count)
174
-
175
312
  self.sim = []
176
- for i in range(sim_count + (1 if has_boundary else 0)):
313
+ for i in range(sim_count):
177
314
  self.sim.append(
178
315
  to_clone.sim[i].clone()
179
316
  if to_clone
@@ -189,16 +326,18 @@ class QrackAceBackend:
189
326
  )
190
327
  )
191
328
 
192
- # You can still "monkey-patch" this, after the constructor.
193
- if "QRACK_QUNIT_SEPARABILITY_THRESHOLD" not in os.environ:
194
- # (1 - 1 / sqrt(2)) / 4 (but empirically tuned)
195
- self.sim[i].set_sdrp(0.073223304703363119)
196
-
197
- self._boundary_sim_id = boundary_sim_id if has_boundary else None
329
+ # You can still call "set_sdrp," after the constructor.
330
+ # if "QRACK_QUNIT_SEPARABILITY_THRESHOLD" not in os.environ:
331
+ # # (1 - 1 / sqrt(2)) / 4 (but empirically tuned)
332
+ # self.sim[i].set_sdrp(0.073223304703363119)
198
333
 
199
334
  def clone(self):
200
335
  return QrackAceBackend(to_clone=self)
201
336
 
337
+ def set_sdrp(self, sdrp):
338
+ for sim in self.sim:
339
+ sim.set_sdrp(sdrp)
340
+
202
341
  def measure_shots_consensus(self, q, s, n_instances=3, threshold=0.1):
203
342
  # Consensus measurement across n_instances independent clones.
204
343
  #
@@ -333,23 +472,35 @@ class QrackAceBackend:
333
472
 
334
473
  @staticmethod
335
474
  def _get_qb_lhv_indices(hq):
336
- # Historically, index 2 (when present) pointed at a private
337
- # classical LHVQubit proxy and had to be special-cased everywhere.
338
- # It is now an ordinary (sim_id, idx) tuple into the shared
339
- # boundary "crossbar" QrackSimulator, so it is just one more
340
- # coupling target like every other index. We keep this helper's
341
- # name and signature for minimal call-site churn; "lhv" is now
342
- # always -1 (no index needs special-casing any more).
475
+ qb = []
343
476
  if len(hq) < 2:
344
477
  qb = [0]
478
+ lhv = -1
345
479
  elif len(hq) < 4:
346
- qb = [0, 1, 2]
480
+ qb = [0, 1]
481
+ lhv = 2
347
482
  else:
348
- qb = [0, 1, 2, 3, 4]
349
- lhv = -1
483
+ qb = [0, 1, 3, 4]
484
+ lhv = 2
350
485
 
351
486
  return qb, lhv
352
487
 
488
+ @staticmethod
489
+ def _get_lhv_bloch_angles(sim):
490
+ # Z axis
491
+ z = sim.bloch[2]
492
+
493
+ # X axis
494
+ x = sim.bloch[0]
495
+
496
+ # Y axis
497
+ y = sim.bloch[1]
498
+
499
+ inclination = math.atan2(math.sqrt(x**2 + y**2), z)
500
+ azimuth = math.atan2(y, x)
501
+
502
+ return azimuth, inclination
503
+
353
504
  def _get_bloch_angles(self, hq):
354
505
  sim = self.sim[hq[0]].clone()
355
506
  q = hq[1]
@@ -392,25 +543,42 @@ class QrackAceBackend:
392
543
 
393
544
  sim.mtrx([m00, m01, m10, m11], q)
394
545
 
546
+ @staticmethod
547
+ def _rotate_lhv_to_bloch(sim, delta_azimuth, delta_inclination):
548
+ # Apply rotation as "Azimuth, Inclination" (AI)
549
+ cosA = math.cos(delta_azimuth)
550
+ sinA = math.sin(delta_azimuth)
551
+ cosI = math.cos(delta_inclination / 2)
552
+ sinI = math.sin(delta_inclination / 2)
553
+
554
+ m00 = complex(cosI, 0)
555
+ m01 = complex(-cosA, sinA) * sinI
556
+ m10 = complex(cosA, sinA) * sinI
557
+ m11 = complex(cosI, 0)
558
+
559
+ sim.mtrx([m00, m01, m10, m11])
560
+
395
561
  def _correct(self, lq, phase=False, skip_rotation=False):
396
562
  hq = self._unpack(lq)
397
563
 
398
564
  if len(hq) == 1:
399
565
  return
400
566
 
401
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
567
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
402
568
 
403
569
  if phase:
404
570
  for q in qb:
405
571
  b = hq[q]
406
572
  self.sim[b[0]].h(b[1])
573
+ b = hq[lhv]
574
+ b.h()
407
575
 
408
576
  if len(hq) == 5:
409
577
  # RMS
410
578
  p = [
411
579
  self.sim[hq[0][0]].prob(hq[0][1]),
412
580
  self.sim[hq[1][0]].prob(hq[1][1]),
413
- self.sim[hq[2][0]].prob(hq[2][1]),
581
+ hq[2].prob(),
414
582
  self.sim[hq[3][0]].prob(hq[3][1]),
415
583
  self.sim[hq[4][0]].prob(hq[4][1]),
416
584
  ]
@@ -426,7 +594,12 @@ class QrackAceBackend:
426
594
  )
427
595
  / 7
428
596
  )
429
- result = ((prms + (1 - qrms)) / 2) >= 0.5
597
+ eff_prob = (prms + (1 - qrms)) / 2
598
+ result = (
599
+ (random.random() < 0.5)
600
+ if (eff_prob - 0.5) == 0.0
601
+ else (eff_prob > 0.5)
602
+ )
430
603
  syndrome = (
431
604
  [1 - p[0], 1 - p[1], 1 - p[2], 1 - p[3], 1 - p[4]]
432
605
  if result
@@ -434,52 +607,91 @@ class QrackAceBackend:
434
607
  )
435
608
  for q in range(5):
436
609
  if syndrome[q] > (0.5 + self._epsilon):
437
- self.sim[hq[q][0]].x(hq[q][1])
610
+ if q == 2:
611
+ hq[q].x()
612
+ else:
613
+ self.sim[hq[q][0]].x(hq[q][1])
438
614
 
439
615
  if not skip_rotation:
440
616
  a, i = [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]
441
617
  a[0], i[0] = self._get_bloch_angles(hq[0])
442
618
  a[1], i[1] = self._get_bloch_angles(hq[1])
443
- a[2], i[2] = self._get_bloch_angles(hq[2])
619
+ a[2], i[2] = QrackAceBackend._get_lhv_bloch_angles(hq[2])
444
620
  a[3], i[3] = self._get_bloch_angles(hq[3])
445
621
  a[4], i[4] = self._get_bloch_angles(hq[4])
446
622
 
447
- a_target = sum(a) / 5
448
- i_target = sum(i) / 5
623
+ a_target = 0
624
+ i_target = 0
625
+ for x in range(5):
626
+ if x == 2:
627
+ continue
628
+ a_target += a[x]
629
+ i_target += i[x]
630
+
631
+ a_target /= 5
632
+ i_target /= 5
449
633
  for x in range(5):
450
- self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
634
+ if x == 2:
635
+ QrackAceBackend._rotate_lhv_to_bloch(
636
+ hq[x], a_target - a[x], i_target - i[x]
637
+ )
638
+ else:
639
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
451
640
 
452
641
  else:
453
642
  # RMS
454
643
  p = [
455
644
  self.sim[hq[0][0]].prob(hq[0][1]),
456
645
  self.sim[hq[1][0]].prob(hq[1][1]),
457
- self.sim[hq[2][0]].prob(hq[2][1]),
646
+ hq[2].prob(),
458
647
  ]
459
648
  # Balancing suggestion from Elara (the custom OpenAI GPT)
460
649
  prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
461
650
  qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
462
- result = ((prms + (1 - qrms)) / 2) >= 0.5
651
+ eff_prob = (prms + (1 - qrms)) / 2
652
+ result = (
653
+ (random.random() < 0.5)
654
+ if (eff_prob - 0.5) == 0.0
655
+ else (eff_prob > 0.5)
656
+ )
463
657
  syndrome = [1 - p[0], 1 - p[1], 1 - p[2]] if result else [p[0], p[1], p[2]]
464
658
  for q in range(3):
465
659
  if syndrome[q] > (0.5 + self._epsilon):
466
- self.sim[hq[q][0]].x(hq[q][1])
660
+ if q == 2:
661
+ hq[q].x()
662
+ else:
663
+ self.sim[hq[q][0]].x(hq[q][1])
467
664
 
468
665
  if not skip_rotation:
469
666
  a, i = [0, 0, 0], [0, 0, 0]
470
667
  a[0], i[0] = self._get_bloch_angles(hq[0])
471
668
  a[1], i[1] = self._get_bloch_angles(hq[1])
472
- a[2], i[2] = self._get_bloch_angles(hq[2])
669
+ a[2], i[2] = QrackAceBackend._get_lhv_bloch_angles(hq[2])
670
+
671
+ a_target = 0
672
+ i_target = 0
673
+ for x in range(3):
674
+ if x == 2:
675
+ continue
676
+ a_target += a[x]
677
+ i_target += i[x]
473
678
 
474
- a_target = sum(a) / 3
475
- i_target = sum(i) / 3
679
+ a_target /= 3
680
+ i_target /= 3
476
681
  for x in range(3):
477
- self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
682
+ if x == 2:
683
+ QrackAceBackend._rotate_lhv_to_bloch(
684
+ hq[x], a_target - a[x], i_target - i[x]
685
+ )
686
+ else:
687
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
478
688
 
479
689
  if phase:
480
690
  for q in qb:
481
691
  b = hq[q]
482
692
  self.sim[b[0]].h(b[1])
693
+ b = hq[lhv]
694
+ b.h()
483
695
 
484
696
  def apply_magnetic_bias(self, q, b):
485
697
  if b == 0:
@@ -487,13 +699,22 @@ class QrackAceBackend:
487
699
  b = math.exp(b)
488
700
  for x in q:
489
701
  hq = self._unpack(x)
490
- for h in hq:
491
- a, i = self._get_bloch_angles(h)
492
- self._rotate_to_bloch(
493
- h,
494
- math.atan(math.tan(a) * b) - a,
495
- math.atan(math.tan(i) * b) - i,
496
- )
702
+ for c in range(len(hq)):
703
+ h = hq[c]
704
+ if c == 2:
705
+ a, i = QrackAceBackend._get_lhv_bloch_angles(h)
706
+ QrackAceBackend._rotate_lhv_to_bloch(
707
+ h,
708
+ math.atan(math.tan(a) * b) - a,
709
+ math.atan(math.tan(i) * b) - i,
710
+ )
711
+ else:
712
+ a, i = self._get_bloch_angles(h)
713
+ self._rotate_to_bloch(
714
+ h,
715
+ math.atan(math.tan(a) * b) - a,
716
+ math.atan(math.tan(i) * b) - i,
717
+ )
497
718
 
498
719
  def u(self, lq, th, ph, lm):
499
720
  hq = self._unpack(lq)
@@ -502,12 +723,15 @@ class QrackAceBackend:
502
723
  self.sim[b[0]].u(b[1], th, ph, lm)
503
724
  return
504
725
 
505
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
726
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
506
727
 
507
728
  for q in qb:
508
729
  b = hq[q]
509
730
  self.sim[b[0]].u(b[1], th, ph, lm)
510
731
 
732
+ b = hq[lhv]
733
+ b.u(th, ph, lm)
734
+
511
735
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
512
736
 
513
737
  def r(self, p, th, lq):
@@ -517,12 +741,20 @@ class QrackAceBackend:
517
741
  self.sim[b[0]].r(p, th, b[1])
518
742
  return
519
743
 
520
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
744
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
521
745
 
522
746
  for q in qb:
523
747
  b = hq[q]
524
748
  self.sim[b[0]].r(p, th, b[1])
525
749
 
750
+ b = hq[lhv]
751
+ if p == Pauli.PauliX:
752
+ b.rx(th)
753
+ elif p == Pauli.PauliY:
754
+ b.ry(th)
755
+ elif p == Pauli.PauliZ:
756
+ b.rz(th)
757
+
526
758
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
527
759
 
528
760
  def h(self, lq):
@@ -534,12 +766,15 @@ class QrackAceBackend:
534
766
 
535
767
  self._correct(lq)
536
768
 
537
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
769
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
538
770
 
539
771
  for q in qb:
540
772
  b = hq[q]
541
773
  self.sim[b[0]].h(b[1])
542
774
 
775
+ b = hq[lhv]
776
+ b.h()
777
+
543
778
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
544
779
 
545
780
  def s(self, lq):
@@ -549,12 +784,15 @@ class QrackAceBackend:
549
784
  self.sim[b[0]].s(b[1])
550
785
  return
551
786
 
552
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
787
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
553
788
 
554
789
  for q in qb:
555
790
  b = hq[q]
556
791
  self.sim[b[0]].s(b[1])
557
792
 
793
+ b = hq[lhv]
794
+ b.s()
795
+
558
796
  def adjs(self, lq):
559
797
  hq = self._unpack(lq)
560
798
  if len(hq) < 2:
@@ -562,12 +800,15 @@ class QrackAceBackend:
562
800
  self.sim[b[0]].adjs(b[1])
563
801
  return
564
802
 
565
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
803
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
566
804
 
567
805
  for q in qb:
568
806
  b = hq[q]
569
807
  self.sim[b[0]].adjs(b[1])
570
808
 
809
+ b = hq[lhv]
810
+ b.adjs()
811
+
571
812
  def x(self, lq):
572
813
  hq = self._unpack(lq)
573
814
  if len(hq) < 2:
@@ -575,12 +816,15 @@ class QrackAceBackend:
575
816
  self.sim[b[0]].x(b[1])
576
817
  return
577
818
 
578
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
819
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
579
820
 
580
821
  for q in qb:
581
822
  b = hq[q]
582
823
  self.sim[b[0]].x(b[1])
583
824
 
825
+ b = hq[lhv]
826
+ b.x()
827
+
584
828
  def y(self, lq):
585
829
  hq = self._unpack(lq)
586
830
  if len(hq) < 2:
@@ -588,12 +832,15 @@ class QrackAceBackend:
588
832
  self.sim[b[0]].y(b[1])
589
833
  return
590
834
 
591
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
835
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
592
836
 
593
837
  for q in qb:
594
838
  b = hq[q]
595
839
  self.sim[b[0]].y(b[1])
596
840
 
841
+ b = hq[lhv]
842
+ b.y()
843
+
597
844
  def z(self, lq):
598
845
  hq = self._unpack(lq)
599
846
  if len(hq) < 2:
@@ -601,12 +848,15 @@ class QrackAceBackend:
601
848
  self.sim[b[0]].z(b[1])
602
849
  return
603
850
 
604
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
851
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
605
852
 
606
853
  for q in qb:
607
854
  b = hq[q]
608
855
  self.sim[b[0]].z(b[1])
609
856
 
857
+ b = hq[lhv]
858
+ b.z()
859
+
610
860
  def t(self, lq):
611
861
  hq = self._unpack(lq)
612
862
  if len(hq) < 2:
@@ -614,12 +864,15 @@ class QrackAceBackend:
614
864
  self.sim[b[0]].t(b[1])
615
865
  return
616
866
 
617
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
867
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
618
868
 
619
869
  for q in qb:
620
870
  b = hq[q]
621
871
  self.sim[b[0]].t(b[1])
622
872
 
873
+ b = hq[lhv]
874
+ b.t()
875
+
623
876
  def adjt(self, lq):
624
877
  hq = self._unpack(lq)
625
878
  if len(hq) < 2:
@@ -627,12 +880,15 @@ class QrackAceBackend:
627
880
  self.sim[b[0]].adjt(b[1])
628
881
  return
629
882
 
630
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
883
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
631
884
 
632
885
  for q in qb:
633
886
  b = hq[q]
634
887
  self.sim[b[0]].adjt(b[1])
635
888
 
889
+ b = hq[lhv]
890
+ b.adjt()
891
+
636
892
  def _get_gate(self, pauli, anti, sim_id):
637
893
  gate = None
638
894
  shadow = None
@@ -671,11 +927,15 @@ class QrackAceBackend:
671
927
 
672
928
  return connected, boundary
673
929
 
674
- def _apply_coupling(self, pauli, anti, qb1, hq1, qb2, hq2, lq1_lr):
930
+ def _apply_coupling(self, pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr):
675
931
  for q1 in qb1:
932
+ if q1 == lhv1:
933
+ continue
676
934
  b1 = hq1[q1]
677
935
  gate_fn, shadow_fn = self._get_gate(pauli, anti, b1[0])
678
936
  for q2 in qb2:
937
+ if q2 == lhv2:
938
+ continue
679
939
  b2 = hq2[q2]
680
940
  if b1[0] == b2[0]:
681
941
  gate_fn([b1[1]], b2[1])
@@ -696,11 +956,18 @@ class QrackAceBackend:
696
956
 
697
957
  self._correct(lq1)
698
958
 
699
- qb1, _ = QrackAceBackend._get_qb_lhv_indices(hq1)
700
- qb2, _ = QrackAceBackend._get_qb_lhv_indices(hq2)
701
- # Apply cross coupling on every qubit, including former-LHV boundary
702
- # qubits, which now live as real qubits in the shared boundary sim.
703
- self._apply_coupling(pauli, anti, qb1, hq1, qb2, hq2, lq1_lr)
959
+ qb1, lhv1 = QrackAceBackend._get_qb_lhv_indices(hq1)
960
+ qb2, lhv2 = QrackAceBackend._get_qb_lhv_indices(hq2)
961
+ # Apply cross coupling on hardware qubits first
962
+ self._apply_coupling(pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr)
963
+ # Apply coupling to the local-hidden-variable target
964
+ if lhv2 >= 0:
965
+ _cpauli_lhv(
966
+ hq1[lhv1].prob() if lhv1 >= 0 else self.sim[hq1[0][0]].prob(hq1[0][1]),
967
+ hq2[lhv2],
968
+ pauli,
969
+ anti,
970
+ )
704
971
 
705
972
  self._correct(lq1, True)
706
973
  if pauli != Pauli.PauliZ:
@@ -797,7 +1064,7 @@ class QrackAceBackend:
797
1064
  p = [
798
1065
  self.sim[hq[0][0]].prob(hq[0][1]),
799
1066
  self.sim[hq[1][0]].prob(hq[1][1]),
800
- self.sim[hq[2][0]].prob(hq[2][1]),
1067
+ hq[2].prob(),
801
1068
  self.sim[hq[3][0]].prob(hq[3][1]),
802
1069
  self.sim[hq[4][0]].prob(hq[4][1]),
803
1070
  ]
@@ -818,7 +1085,7 @@ class QrackAceBackend:
818
1085
  p = [
819
1086
  self.sim[hq[0][0]].prob(hq[0][1]),
820
1087
  self.sim[hq[1][0]].prob(hq[1][1]),
821
- self.sim[hq[2][0]].prob(hq[2][1]),
1088
+ hq[2].prob(),
822
1089
  ]
823
1090
  # Balancing suggestion from Elara (the custom OpenAI GPT)
824
1091
  prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
@@ -835,7 +1102,7 @@ class QrackAceBackend:
835
1102
  p = self.prob(lq)
836
1103
  result = ((p + self._epsilon) >= 1) or (random.random() < p)
837
1104
 
838
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
1105
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
839
1106
 
840
1107
  for q in qb:
841
1108
  b = hq[q]
@@ -846,6 +1113,11 @@ class QrackAceBackend:
846
1113
  else:
847
1114
  self.sim[b[0]].force_m(b[1], result)
848
1115
 
1116
+ b = hq[lhv]
1117
+ b.reset()
1118
+ if result:
1119
+ b.x()
1120
+
849
1121
  return result
850
1122
 
851
1123
  def force_m(self, lq, result):
@@ -856,7 +1128,7 @@ class QrackAceBackend:
856
1128
 
857
1129
  self._correct(lq)
858
1130
 
859
- qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
1131
+ qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
860
1132
 
861
1133
  for q in qb:
862
1134
  b = hq[q]
@@ -867,6 +1139,11 @@ class QrackAceBackend:
867
1139
  else:
868
1140
  self.sim[b[0]].force_m(b[1], result)
869
1141
 
1142
+ b = hq[lhv]
1143
+ b.reset()
1144
+ if result:
1145
+ b.x()
1146
+
870
1147
  return result
871
1148
 
872
1149
  def m_all(self):
@@ -1232,22 +1509,25 @@ class QrackAceBackend:
1232
1509
  return self._coupling_map
1233
1510
 
1234
1511
  coupling_map = set()
1235
- rows, cols = self._row_length, self._col_length
1512
+ # self._row_length is the number of qubits per row (the row's span,
1513
+ # i.e. the column count); self._col_length is the number of rows.
1514
+ num_rows, num_cols = self._col_length, self._row_length
1236
1515
 
1237
- # Map each column index to its full list of logical qubit indices
1516
+ # Row-major logical index, strided by the actual row span.
1238
1517
  def logical_index(row, col):
1239
- return row * cols + col
1518
+ return row * num_cols + col
1240
1519
 
1241
- for col in range(cols):
1242
- connected_cols, _ = self._get_connected(col, False)
1243
- for row in range(rows):
1244
- connected_rows, _ = self._get_connected(row, False)
1520
+ for row in range(num_rows):
1521
+ connected_rows, _ = self._get_connected(row, True)
1522
+ for col in range(num_cols):
1523
+ connected_cols, _ = self._get_connected(col, False)
1245
1524
  a = logical_index(row, col)
1246
- for c in connected_cols:
1247
- for r in connected_rows:
1525
+ for r in connected_rows:
1526
+ for c in connected_cols:
1248
1527
  b = logical_index(r, c)
1249
1528
  if a != b:
1250
1529
  coupling_map.add((a, b))
1530
+ coupling_map.add((b, a))
1251
1531
 
1252
1532
  self._coupling_map = sorted(coupling_map)
1253
1533
 
@@ -1261,9 +1541,11 @@ class QrackAceBackend:
1261
1541
  )
1262
1542
  noise_model = NoiseModel()
1263
1543
 
1264
- # Single-qubit depolarizing only on boundary qubits
1265
1544
  boundary_qubits = set()
1266
1545
  for a, b in self.get_logical_coupling_map():
1546
+ if a > b:
1547
+ #Skip duplicates
1548
+ continue
1267
1549
  col_a = a % self._row_length
1268
1550
  col_b = b % self._row_length
1269
1551
  is_long_a = self._is_col_long_range[col_a]
@@ -1274,12 +1556,6 @@ class QrackAceBackend:
1274
1556
  if not is_long_b:
1275
1557
  boundary_qubits.add(b)
1276
1558
 
1277
- for q in boundary_qubits:
1278
- for gate in ["u", "u1", "u2", "u3", "h", "x", "y", "z",
1279
- "s", "sdg", "t", "tdg", "rx", "ry", "rz"]:
1280
- noise_model.add_quantum_error(
1281
- depolarizing_error(x, 1), gate, [q])
1282
-
1283
1559
  # Two-qubit depolarizing on boundary-crossing and boundary-adjacent gates
1284
1560
  for a, b in self.get_logical_coupling_map():
1285
1561
  col_a = a % self._row_length
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cuda-complex128
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -7,7 +7,7 @@ from setuptools import setup
7
7
  from setuptools.command.build_py import build_py
8
8
 
9
9
 
10
- VERSION = "2.2.1"
10
+ VERSION = "2.2.2"
11
11
 
12
12
  # Read long description from README.
13
13
  README_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'README.md')