pyqrack-cuda-complex128 2.2.2__tar.gz → 2.2.4__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.2 → pyqrack_cuda_complex128-2.2.4}/Makefile +1 -1
  2. {pyqrack_cuda_complex128-2.2.2/pyqrack_cuda_complex128.egg-info → pyqrack_cuda_complex128-2.2.4}/PKG-INFO +1 -1
  3. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_ace_backend.py +269 -202
  4. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4/pyqrack_cuda_complex128.egg-info}/PKG-INFO +1 -1
  5. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/setup.py +1 -1
  6. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/LICENSE +0 -0
  7. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/MANIFEST.in +0 -0
  8. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/README.md +0 -0
  9. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyproject.toml +0 -0
  10. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/__init__.py +0 -0
  11. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/neuron_activation_fn.py +0 -0
  12. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/pauli.py +0 -0
  13. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_circuit.py +0 -0
  14. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  15. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_neuron.py +0 -0
  16. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  17. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_simulator.py +0 -0
  18. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_stabilizer.py +0 -0
  19. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_system/__init__.py +0 -0
  20. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/qrack_system/qrack_system.py +0 -0
  21. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/quimb_circuit_type.py +0 -0
  22. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/stats/__init__.py +0 -0
  23. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/stats/load_quantized_data.py +0 -0
  24. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack/stats/quantize_by_range.py +0 -0
  25. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack_cuda_complex128.egg-info/SOURCES.txt +0 -0
  26. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack_cuda_complex128.egg-info/dependency_links.txt +0 -0
  27. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack_cuda_complex128.egg-info/not-zip-safe +0 -0
  28. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack_cuda_complex128.egg-info/requires.txt +0 -0
  29. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/pyqrack_cuda_complex128.egg-info/top_level.txt +0 -0
  30. {pyqrack_cuda_complex128-2.2.2 → pyqrack_cuda_complex128-2.2.4}/setup.cfg +0 -0
@@ -30,7 +30,7 @@ build-deps:
30
30
  rm -rf pyqrack/qrack_system/qrack_cl_precompile
31
31
  ifneq ($(OS),Windows_NT)
32
32
  ifeq ($(QRACK_PRESENT),)
33
- git clone https://github.com/unitaryfund/qrack.git; cd qrack; git checkout ccb9cca0726e48c598696b51a66fb984ff8ab0f1; cd ..
33
+ git clone https://github.com/unitaryfund/qrack.git; cd qrack; git checkout 01dbf96198e4df51cebcf23e506571cee0927244; cd ..
34
34
  endif
35
35
  mkdir -p qrack/build
36
36
  ifeq ($(UNAME_S),Linux)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cuda-complex128
3
- Version: 2.2.2
3
+ Version: 2.2.4
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -191,7 +191,6 @@ def _cpauli_lhv(prob, targ, axis, anti, theta=math.pi):
191
191
  elif axis == Pauli.PauliZ:
192
192
  targ.rz(effective_theta)
193
193
 
194
-
195
194
  class QrackAceBackend:
196
195
  """A back end for elided quantum error correction
197
196
 
@@ -212,6 +211,12 @@ class QrackAceBackend:
212
211
  is_transpose(bool): Rows are long if False, columns are long if True
213
212
  """
214
213
 
214
+ # Sweepable vote weights for the 4-source boundary correction pool:
215
+ # [slot0, slot1, slot2, lhv]. Must sum to an odd number so the
216
+ # underlying hard-vote tally (independent of the continuous RMS
217
+ # formula) is always tie-free.
218
+ _LHV_VOTE_WEIGHTS = [2, 1, 1, 1]
219
+
215
220
  def __init__(
216
221
  self,
217
222
  qubit_count=1,
@@ -276,7 +281,20 @@ class QrackAceBackend:
276
281
  self._is_row_long_range[-1] = False
277
282
  sim_count = col_patch_count * row_patch_count
278
283
 
284
+ # Boundary qubits no longer carry a private classical LHV proxy.
285
+ # Instead, every boundary site (row- and/or column-boundary alike)
286
+ # gets a real qubit in one single, shared "crossbar" QrackSimulator.
287
+ # That simulator's own greedy elision (set_sdrp) is trusted to
288
+ # automatically factor apart whatever boundary sites turn out to be
289
+ # separable (e.g. disjoint rails, rail intersections), exactly the
290
+ # same way it already factors apart unentangled subspaces within
291
+ # any other single QrackSimulator instance. We don't need to special
292
+ # -case "crossbar intersections" by hand; the elision does it for us.
293
+ boundary_sim_id = sim_count
294
+ boundary_count = 0
295
+
279
296
  self._qubits = []
297
+ self._lhv = {}
280
298
  sim_counts = [0] * sim_count
281
299
  sim_id = 0
282
300
  tot_qubits = 0
@@ -290,8 +308,11 @@ class QrackAceBackend:
290
308
  qubit.append((t_sim_id, sim_counts[t_sim_id]))
291
309
  sim_counts[t_sim_id] += 1
292
310
 
293
- qubit.append(
294
- LHVQubit(to_clone=(to_clone._qubits[tot_qubits][2] if to_clone else None))
311
+ qubit.append((boundary_sim_id, boundary_count))
312
+ boundary_count += 1
313
+
314
+ self._lhv[tot_qubits] = LHVQubit(
315
+ to_clone=(to_clone._lhv[tot_qubits] if to_clone else None)
295
316
  )
296
317
 
297
318
  if (not c) and (not r):
@@ -309,8 +330,23 @@ class QrackAceBackend:
309
330
  self._qubits.append(qubit)
310
331
  tot_qubits += 1
311
332
 
333
+ # The crossbar's size is fixed by how many boundary sites exist.
334
+ # When there are none (e.g. a grid small enough, relative to
335
+ # long_range_rows/columns, that the whole thing is "fully
336
+ # connected" with no QEC boundary at all), we must NOT allocate a
337
+ # 0-qubit QrackSimulator for the crossbar: a 0-qubit QrackSimulator
338
+ # can be constructed, but calling .clone() on one crashes the
339
+ # native Qrack core (segfault), and clone() is called routinely by
340
+ # measure_shots()/clone(). So the boundary sim is only created when
341
+ # boundary_count > 0, exactly mirroring how the original LHV-based
342
+ # code never instantiated anything for the boundary case when
343
+ # there were no boundary sites.
344
+ has_boundary = boundary_count > 0
345
+ if has_boundary:
346
+ sim_counts.append(boundary_count)
347
+
312
348
  self.sim = []
313
- for i in range(sim_count):
349
+ for i in range(sim_count + (1 if has_boundary else 0)):
314
350
  self.sim.append(
315
351
  to_clone.sim[i].clone()
316
352
  if to_clone
@@ -331,13 +367,11 @@ class QrackAceBackend:
331
367
  # # (1 - 1 / sqrt(2)) / 4 (but empirically tuned)
332
368
  # self.sim[i].set_sdrp(0.073223304703363119)
333
369
 
370
+ self._boundary_sim_id = boundary_sim_id if has_boundary else None
371
+
334
372
  def clone(self):
335
373
  return QrackAceBackend(to_clone=self)
336
374
 
337
- def set_sdrp(self, sdrp):
338
- for sim in self.sim:
339
- sim.set_sdrp(sdrp)
340
-
341
375
  def measure_shots_consensus(self, q, s, n_instances=3, threshold=0.1):
342
376
  # Consensus measurement across n_instances independent clones.
343
377
  #
@@ -405,6 +439,24 @@ class QrackAceBackend:
405
439
  p1 = self.sim[q1[0]].prob(q1[1]) if isinstance(q1, tuple) else q1.prob()
406
440
  p2 = self.sim[q2[0]].prob(q2[1]) if isinstance(q2, tuple) else q2.prob()
407
441
 
442
+ # When p1 and p2 are within floating-point noise of each other
443
+ # (self._epsilon), they carry no real information about which
444
+ # qubit is "more likely 1" -- e.g. a fresh target right after
445
+ # _cx_shadow's H() always reads ~0.5, indistinguishably from a
446
+ # genuinely-mixed control. This shadow mechanism can only return
447
+ # ONE definite classical pick per call, so resolving every such
448
+ # near-tie to a FIXED qubit (as a plain "<" or "<=" comparison
449
+ # would, once p1/p2 are within epsilon of each other) reintroduces
450
+ # a systematic bias -- it just moves the bias from "favors q1" to
451
+ # "always favors q2" instead of removing it. The aggregate
452
+ # statistics this shadow is meant to approximate (e.g. a real CX
453
+ # from a maximally-mixed control onto a fresh target makes BOTH
454
+ # qubits individually read prob=0.5, perfectly correlated) are
455
+ # only reproduced, across many circuit instances, if a genuine
456
+ # tie is broken at random rather than by a fixed rule.
457
+ if abs(p1 - p2) <= self._epsilon:
458
+ return (p1, q2) if random.random() < 0.5 else (p2, q1)
459
+
408
460
  if p1 < p2:
409
461
  return p2, q1
410
462
 
@@ -412,7 +464,16 @@ class QrackAceBackend:
412
464
 
413
465
  def _cz_shadow(self, q1, q2):
414
466
  prob_max, t = self._ct_pair_prob(q1, q2)
415
- if prob_max > 0.5:
467
+ # NOTE: this must be ">=", not ">". H() applied to any qubit in a
468
+ # definite computational-basis state (e.g. a fresh boundary/ancilla
469
+ # qubit, as in _cx_shadow's H-sandwich) lands at EXACTLY prob=0.5,
470
+ # deterministically -- this is the ordinary case, not a rare
471
+ # floating-point tie. A strict "> 0.5" therefore silently no-ops
472
+ # the shadow gate every time it targets a fresh qubit, which
473
+ # breaks entanglement transfer for the extremely common case of a
474
+ # CX/CY/CZ from a maximally-mixed-looking control onto a fresh
475
+ # target (e.g. H(0); cx(0,1) for a Bell pair).
476
+ if prob_max >= (0.5 - self._epsilon):
416
477
  if isinstance(t, tuple):
417
478
  self.sim[t[0]].z(t[1])
418
479
  else:
@@ -472,35 +533,23 @@ class QrackAceBackend:
472
533
 
473
534
  @staticmethod
474
535
  def _get_qb_lhv_indices(hq):
475
- qb = []
536
+ # Historically, index 2 (when present) pointed at a private
537
+ # classical LHVQubit proxy and had to be special-cased everywhere.
538
+ # It is now an ordinary (sim_id, idx) tuple into the shared
539
+ # boundary "crossbar" QrackSimulator, so it is just one more
540
+ # coupling target like every other index. We keep this helper's
541
+ # name and signature for minimal call-site churn; "lhv" is now
542
+ # always -1 (no index needs special-casing any more).
476
543
  if len(hq) < 2:
477
544
  qb = [0]
478
- lhv = -1
479
545
  elif len(hq) < 4:
480
- qb = [0, 1]
481
- lhv = 2
546
+ qb = [0, 1, 2]
482
547
  else:
483
- qb = [0, 1, 3, 4]
484
- lhv = 2
548
+ qb = [0, 1, 2, 3, 4]
549
+ lhv = -1
485
550
 
486
551
  return qb, lhv
487
552
 
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
-
504
553
  def _get_bloch_angles(self, hq):
505
554
  sim = self.sim[hq[0]].clone()
506
555
  q = hq[1]
@@ -543,9 +592,17 @@ class QrackAceBackend:
543
592
 
544
593
  sim.mtrx([m00, m01, m10, m11], q)
545
594
 
595
+ @staticmethod
596
+ def _get_lhv_bloch_angles(sim):
597
+ z = sim.bloch[2]
598
+ x = sim.bloch[0]
599
+ y = sim.bloch[1]
600
+ inclination = math.atan2(math.sqrt(x**2 + y**2), z)
601
+ azimuth = math.atan2(y, x)
602
+ return azimuth, inclination
603
+
546
604
  @staticmethod
547
605
  def _rotate_lhv_to_bloch(sim, delta_azimuth, delta_inclination):
548
- # Apply rotation as "Azimuth, Inclination" (AI)
549
606
  cosA = math.cos(delta_azimuth)
550
607
  sinA = math.sin(delta_azimuth)
551
608
  cosI = math.cos(delta_inclination / 2)
@@ -564,21 +621,21 @@ class QrackAceBackend:
564
621
  if len(hq) == 1:
565
622
  return
566
623
 
567
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
624
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
568
625
 
569
626
  if phase:
570
627
  for q in qb:
571
628
  b = hq[q]
572
629
  self.sim[b[0]].h(b[1])
573
- b = hq[lhv]
574
- b.h()
630
+ if lq in self._lhv:
631
+ self._lhv[lq].h()
575
632
 
576
633
  if len(hq) == 5:
577
634
  # RMS
578
635
  p = [
579
636
  self.sim[hq[0][0]].prob(hq[0][1]),
580
637
  self.sim[hq[1][0]].prob(hq[1][1]),
581
- hq[2].prob(),
638
+ self.sim[hq[2][0]].prob(hq[2][1]),
582
639
  self.sim[hq[3][0]].prob(hq[3][1]),
583
640
  self.sim[hq[4][0]].prob(hq[4][1]),
584
641
  ]
@@ -597,8 +654,8 @@ class QrackAceBackend:
597
654
  eff_prob = (prms + (1 - qrms)) / 2
598
655
  result = (
599
656
  (random.random() < 0.5)
600
- if (eff_prob - 0.5) == 0.0
601
- else (eff_prob > 0.5)
657
+ if abs(eff_prob - 0.5) <= self._epsilon
658
+ else (eff_prob >= 0.5)
602
659
  )
603
660
  syndrome = (
604
661
  [1 - p[0], 1 - p[1], 1 - p[2], 1 - p[3], 1 - p[4]]
@@ -607,91 +664,91 @@ class QrackAceBackend:
607
664
  )
608
665
  for q in range(5):
609
666
  if syndrome[q] > (0.5 + self._epsilon):
610
- if q == 2:
611
- hq[q].x()
612
- else:
613
- self.sim[hq[q][0]].x(hq[q][1])
667
+ self.sim[hq[q][0]].x(hq[q][1])
614
668
 
615
669
  if not skip_rotation:
616
670
  a, i = [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]
617
671
  a[0], i[0] = self._get_bloch_angles(hq[0])
618
672
  a[1], i[1] = self._get_bloch_angles(hq[1])
619
- a[2], i[2] = QrackAceBackend._get_lhv_bloch_angles(hq[2])
673
+ a[2], i[2] = self._get_bloch_angles(hq[2])
620
674
  a[3], i[3] = self._get_bloch_angles(hq[3])
621
675
  a[4], i[4] = self._get_bloch_angles(hq[4])
622
676
 
623
- a_target = 0
624
- i_target = 0
677
+ a_target = sum(a) / 5
678
+ i_target = sum(i) / 5
625
679
  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
633
- for x in range(5):
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])
680
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
640
681
 
641
682
  else:
642
- # RMS
643
- p = [
644
- self.sim[hq[0][0]].prob(hq[0][1]),
645
- self.sim[hq[1][0]].prob(hq[1][1]),
646
- hq[2].prob(),
647
- ]
648
- # Balancing suggestion from Elara (the custom OpenAI GPT)
649
- prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
650
- qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
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
- )
657
- syndrome = [1 - p[0], 1 - p[1], 1 - p[2]] if result else [p[0], p[1], p[2]]
658
- for q in range(3):
659
- if syndrome[q] > (0.5 + self._epsilon):
660
- if q == 2:
661
- hq[q].x()
662
- else:
683
+ lhv = self._lhv.get(lq)
684
+ if lhv is None:
685
+ # RMS
686
+ p = [
687
+ self.sim[hq[0][0]].prob(hq[0][1]),
688
+ self.sim[hq[1][0]].prob(hq[1][1]),
689
+ self.sim[hq[2][0]].prob(hq[2][1]),
690
+ ]
691
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
692
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
693
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
694
+ eff_prob = (prms + (1 - qrms)) / 2
695
+ result = (
696
+ (random.random() < 0.5)
697
+ if abs(eff_prob - 0.5) <= self._epsilon
698
+ else (eff_prob >= 0.5)
699
+ )
700
+ syndrome = [1 - p[0], 1 - p[1], 1 - p[2]] if result else [p[0], p[1], p[2]]
701
+ for q in range(3):
702
+ if syndrome[q] > (0.5 + self._epsilon):
663
703
  self.sim[hq[q][0]].x(hq[q][1])
704
+ else:
705
+ # Weighted 4-source vote: slot0, slot1, slot2 (real qubits)
706
+ # plus lhv (continuous, non-collapsing proxy). Weights sum
707
+ # to an odd total (QrackAceBackend._LHV_VOTE_WEIGHTS), so
708
+ # the underlying hard-vote tally is always tie-free, while
709
+ # the RMS formula itself generalizes the existing unweighted
710
+ # one (and reduces to it exactly when all weights are equal).
711
+ p = [
712
+ self.sim[hq[0][0]].prob(hq[0][1]),
713
+ self.sim[hq[1][0]].prob(hq[1][1]),
714
+ self.sim[hq[2][0]].prob(hq[2][1]),
715
+ lhv.prob(),
716
+ ]
717
+ w = QrackAceBackend._LHV_VOTE_WEIGHTS
718
+ sw = sum(w)
719
+ prms = math.sqrt(sum(wi * pi**2 for wi, pi in zip(w, p)) / sw)
720
+ qrms = math.sqrt(sum(wi * (1 - pi) ** 2 for wi, pi in zip(w, p)) / sw)
721
+ eff_prob = (prms + (1 - qrms)) / 2
722
+ result = (
723
+ (random.random() < 0.5)
724
+ if abs(eff_prob - 0.5) <= self._epsilon
725
+ else (eff_prob >= 0.5)
726
+ )
727
+ syndrome = [1 - x for x in p] if result else list(p)
728
+ for q in range(3):
729
+ if syndrome[q] > (0.5 + self._epsilon):
730
+ self.sim[hq[q][0]].x(hq[q][1])
731
+ # The LHV proxy is never hard-collapsed via x(); only ever
732
+ # updated by continuous rotation, preserving the property
733
+ # that makes it useful as a non-collapsing stabilizing term.
664
734
 
665
735
  if not skip_rotation:
666
736
  a, i = [0, 0, 0], [0, 0, 0]
667
737
  a[0], i[0] = self._get_bloch_angles(hq[0])
668
738
  a[1], i[1] = self._get_bloch_angles(hq[1])
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]
739
+ a[2], i[2] = self._get_bloch_angles(hq[2])
678
740
 
679
- a_target /= 3
680
- i_target /= 3
741
+ a_target = sum(a) / 3
742
+ i_target = sum(i) / 3
681
743
  for x in range(3):
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])
744
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
688
745
 
689
746
  if phase:
690
747
  for q in qb:
691
748
  b = hq[q]
692
749
  self.sim[b[0]].h(b[1])
693
- b = hq[lhv]
694
- b.h()
750
+ if lq in self._lhv:
751
+ self._lhv[lq].h()
695
752
 
696
753
  def apply_magnetic_bias(self, q, b):
697
754
  if b == 0:
@@ -699,22 +756,13 @@ class QrackAceBackend:
699
756
  b = math.exp(b)
700
757
  for x in q:
701
758
  hq = self._unpack(x)
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
- )
759
+ for h in hq:
760
+ a, i = self._get_bloch_angles(h)
761
+ self._rotate_to_bloch(
762
+ h,
763
+ math.atan(math.tan(a) * b) - a,
764
+ math.atan(math.tan(i) * b) - i,
765
+ )
718
766
 
719
767
  def u(self, lq, th, ph, lm):
720
768
  hq = self._unpack(lq)
@@ -723,14 +771,15 @@ class QrackAceBackend:
723
771
  self.sim[b[0]].u(b[1], th, ph, lm)
724
772
  return
725
773
 
726
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
774
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
727
775
 
728
776
  for q in qb:
729
777
  b = hq[q]
730
778
  self.sim[b[0]].u(b[1], th, ph, lm)
731
779
 
732
- b = hq[lhv]
733
- b.u(th, ph, lm)
780
+ lhv = self._lhv.get(lq)
781
+ if lhv is not None:
782
+ lhv.u(th, ph, lm)
734
783
 
735
784
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
736
785
 
@@ -741,19 +790,20 @@ class QrackAceBackend:
741
790
  self.sim[b[0]].r(p, th, b[1])
742
791
  return
743
792
 
744
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
793
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
745
794
 
746
795
  for q in qb:
747
796
  b = hq[q]
748
797
  self.sim[b[0]].r(p, th, b[1])
749
798
 
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)
799
+ lhv = self._lhv.get(lq)
800
+ if lhv is not None:
801
+ if p == Pauli.PauliX:
802
+ lhv.rx(th)
803
+ elif p == Pauli.PauliY:
804
+ lhv.ry(th)
805
+ elif p == Pauli.PauliZ:
806
+ lhv.rz(th)
757
807
 
758
808
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
759
809
 
@@ -766,14 +816,15 @@ class QrackAceBackend:
766
816
 
767
817
  self._correct(lq)
768
818
 
769
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
819
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
770
820
 
771
821
  for q in qb:
772
822
  b = hq[q]
773
823
  self.sim[b[0]].h(b[1])
774
824
 
775
- b = hq[lhv]
776
- b.h()
825
+ lhv = self._lhv.get(lq)
826
+ if lhv is not None:
827
+ lhv.h()
777
828
 
778
829
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
779
830
 
@@ -784,14 +835,15 @@ class QrackAceBackend:
784
835
  self.sim[b[0]].s(b[1])
785
836
  return
786
837
 
787
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
838
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
788
839
 
789
840
  for q in qb:
790
841
  b = hq[q]
791
842
  self.sim[b[0]].s(b[1])
792
843
 
793
- b = hq[lhv]
794
- b.s()
844
+ lhv = self._lhv.get(lq)
845
+ if lhv is not None:
846
+ lhv.s()
795
847
 
796
848
  def adjs(self, lq):
797
849
  hq = self._unpack(lq)
@@ -800,14 +852,15 @@ class QrackAceBackend:
800
852
  self.sim[b[0]].adjs(b[1])
801
853
  return
802
854
 
803
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
855
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
804
856
 
805
857
  for q in qb:
806
858
  b = hq[q]
807
859
  self.sim[b[0]].adjs(b[1])
808
860
 
809
- b = hq[lhv]
810
- b.adjs()
861
+ lhv = self._lhv.get(lq)
862
+ if lhv is not None:
863
+ lhv.adjs()
811
864
 
812
865
  def x(self, lq):
813
866
  hq = self._unpack(lq)
@@ -816,14 +869,15 @@ class QrackAceBackend:
816
869
  self.sim[b[0]].x(b[1])
817
870
  return
818
871
 
819
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
872
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
820
873
 
821
874
  for q in qb:
822
875
  b = hq[q]
823
876
  self.sim[b[0]].x(b[1])
824
877
 
825
- b = hq[lhv]
826
- b.x()
878
+ lhv = self._lhv.get(lq)
879
+ if lhv is not None:
880
+ lhv.x()
827
881
 
828
882
  def y(self, lq):
829
883
  hq = self._unpack(lq)
@@ -832,14 +886,15 @@ class QrackAceBackend:
832
886
  self.sim[b[0]].y(b[1])
833
887
  return
834
888
 
835
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
889
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
836
890
 
837
891
  for q in qb:
838
892
  b = hq[q]
839
893
  self.sim[b[0]].y(b[1])
840
894
 
841
- b = hq[lhv]
842
- b.y()
895
+ lhv = self._lhv.get(lq)
896
+ if lhv is not None:
897
+ lhv.y()
843
898
 
844
899
  def z(self, lq):
845
900
  hq = self._unpack(lq)
@@ -848,14 +903,15 @@ class QrackAceBackend:
848
903
  self.sim[b[0]].z(b[1])
849
904
  return
850
905
 
851
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
906
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
852
907
 
853
908
  for q in qb:
854
909
  b = hq[q]
855
910
  self.sim[b[0]].z(b[1])
856
911
 
857
- b = hq[lhv]
858
- b.z()
912
+ lhv = self._lhv.get(lq)
913
+ if lhv is not None:
914
+ lhv.z()
859
915
 
860
916
  def t(self, lq):
861
917
  hq = self._unpack(lq)
@@ -864,14 +920,15 @@ class QrackAceBackend:
864
920
  self.sim[b[0]].t(b[1])
865
921
  return
866
922
 
867
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
923
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
868
924
 
869
925
  for q in qb:
870
926
  b = hq[q]
871
927
  self.sim[b[0]].t(b[1])
872
928
 
873
- b = hq[lhv]
874
- b.t()
929
+ lhv = self._lhv.get(lq)
930
+ if lhv is not None:
931
+ lhv.t()
875
932
 
876
933
  def adjt(self, lq):
877
934
  hq = self._unpack(lq)
@@ -880,14 +937,15 @@ class QrackAceBackend:
880
937
  self.sim[b[0]].adjt(b[1])
881
938
  return
882
939
 
883
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
940
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
884
941
 
885
942
  for q in qb:
886
943
  b = hq[q]
887
944
  self.sim[b[0]].adjt(b[1])
888
945
 
889
- b = hq[lhv]
890
- b.adjt()
946
+ lhv = self._lhv.get(lq)
947
+ if lhv is not None:
948
+ lhv.adjt()
891
949
 
892
950
  def _get_gate(self, pauli, anti, sim_id):
893
951
  gate = None
@@ -927,15 +985,11 @@ class QrackAceBackend:
927
985
 
928
986
  return connected, boundary
929
987
 
930
- def _apply_coupling(self, pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr):
988
+ def _apply_coupling(self, pauli, anti, qb1, hq1, qb2, hq2, lq1_lr):
931
989
  for q1 in qb1:
932
- if q1 == lhv1:
933
- continue
934
990
  b1 = hq1[q1]
935
991
  gate_fn, shadow_fn = self._get_gate(pauli, anti, b1[0])
936
992
  for q2 in qb2:
937
- if q2 == lhv2:
938
- continue
939
993
  b2 = hq2[q2]
940
994
  if b1[0] == b2[0]:
941
995
  gate_fn([b1[1]], b2[1])
@@ -956,18 +1010,15 @@ class QrackAceBackend:
956
1010
 
957
1011
  self._correct(lq1)
958
1012
 
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
- )
1013
+ qb1, _ = QrackAceBackend._get_qb_lhv_indices(hq1)
1014
+ qb2, _ = QrackAceBackend._get_qb_lhv_indices(hq2)
1015
+ # Apply cross coupling on every qubit, including former-LHV boundary
1016
+ # qubits, which now live as real qubits in the shared boundary sim.
1017
+ self._apply_coupling(pauli, anti, qb1, hq1, qb2, hq2, lq1_lr)
1018
+
1019
+ if lq2 in self._lhv:
1020
+ ctrl_prob = self.sim[hq1[0][0]].prob(hq1[0][1])
1021
+ _cpauli_lhv(ctrl_prob, self._lhv[lq2], pauli, anti)
971
1022
 
972
1023
  self._correct(lq1, True)
973
1024
  if pauli != Pauli.PauliZ:
@@ -1064,7 +1115,7 @@ class QrackAceBackend:
1064
1115
  p = [
1065
1116
  self.sim[hq[0][0]].prob(hq[0][1]),
1066
1117
  self.sim[hq[1][0]].prob(hq[1][1]),
1067
- hq[2].prob(),
1118
+ self.sim[hq[2][0]].prob(hq[2][1]),
1068
1119
  self.sim[hq[3][0]].prob(hq[3][1]),
1069
1120
  self.sim[hq[4][0]].prob(hq[4][1]),
1070
1121
  ]
@@ -1081,15 +1132,28 @@ class QrackAceBackend:
1081
1132
  / 7
1082
1133
  )
1083
1134
  else:
1084
- # RMS
1085
- p = [
1086
- self.sim[hq[0][0]].prob(hq[0][1]),
1087
- self.sim[hq[1][0]].prob(hq[1][1]),
1088
- hq[2].prob(),
1089
- ]
1090
- # Balancing suggestion from Elara (the custom OpenAI GPT)
1091
- prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
1092
- qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
1135
+ lhv = self._lhv.get(lq)
1136
+ if lhv is None:
1137
+ # RMS
1138
+ p = [
1139
+ self.sim[hq[0][0]].prob(hq[0][1]),
1140
+ self.sim[hq[1][0]].prob(hq[1][1]),
1141
+ self.sim[hq[2][0]].prob(hq[2][1]),
1142
+ ]
1143
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
1144
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
1145
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
1146
+ else:
1147
+ p = [
1148
+ self.sim[hq[0][0]].prob(hq[0][1]),
1149
+ self.sim[hq[1][0]].prob(hq[1][1]),
1150
+ self.sim[hq[2][0]].prob(hq[2][1]),
1151
+ lhv.prob(),
1152
+ ]
1153
+ w = QrackAceBackend._LHV_VOTE_WEIGHTS
1154
+ sw = sum(w)
1155
+ prms = math.sqrt(sum(wi * pi**2 for wi, pi in zip(w, p)) / sw)
1156
+ qrms = math.sqrt(sum(wi * (1 - pi) ** 2 for wi, pi in zip(w, p)) / sw)
1093
1157
 
1094
1158
  return (prms + (1 - qrms)) / 2
1095
1159
 
@@ -1102,7 +1166,7 @@ class QrackAceBackend:
1102
1166
  p = self.prob(lq)
1103
1167
  result = ((p + self._epsilon) >= 1) or (random.random() < p)
1104
1168
 
1105
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1169
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
1106
1170
 
1107
1171
  for q in qb:
1108
1172
  b = hq[q]
@@ -1113,10 +1177,11 @@ class QrackAceBackend:
1113
1177
  else:
1114
1178
  self.sim[b[0]].force_m(b[1], result)
1115
1179
 
1116
- b = hq[lhv]
1117
- b.reset()
1118
- if result:
1119
- b.x()
1180
+ lhv = self._lhv.get(lq)
1181
+ if lhv is not None:
1182
+ lhv.reset()
1183
+ if result:
1184
+ lhv.x()
1120
1185
 
1121
1186
  return result
1122
1187
 
@@ -1128,7 +1193,7 @@ class QrackAceBackend:
1128
1193
 
1129
1194
  self._correct(lq)
1130
1195
 
1131
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1196
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
1132
1197
 
1133
1198
  for q in qb:
1134
1199
  b = hq[q]
@@ -1139,10 +1204,11 @@ class QrackAceBackend:
1139
1204
  else:
1140
1205
  self.sim[b[0]].force_m(b[1], result)
1141
1206
 
1142
- b = hq[lhv]
1143
- b.reset()
1144
- if result:
1145
- b.x()
1207
+ lhv = self._lhv.get(lq)
1208
+ if lhv is not None:
1209
+ lhv.reset()
1210
+ if result:
1211
+ lhv.x()
1146
1212
 
1147
1213
  return result
1148
1214
 
@@ -1509,25 +1575,22 @@ class QrackAceBackend:
1509
1575
  return self._coupling_map
1510
1576
 
1511
1577
  coupling_map = set()
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
1578
+ rows, cols = self._row_length, self._col_length
1515
1579
 
1516
- # Row-major logical index, strided by the actual row span.
1580
+ # Map each column index to its full list of logical qubit indices
1517
1581
  def logical_index(row, col):
1518
- return row * num_cols + col
1582
+ return row * cols + col
1519
1583
 
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)
1584
+ for col in range(cols):
1585
+ connected_cols, _ = self._get_connected(col, False)
1586
+ for row in range(rows):
1587
+ connected_rows, _ = self._get_connected(row, False)
1524
1588
  a = logical_index(row, col)
1525
- for r in connected_rows:
1526
- for c in connected_cols:
1589
+ for c in connected_cols:
1590
+ for r in connected_rows:
1527
1591
  b = logical_index(r, c)
1528
1592
  if a != b:
1529
1593
  coupling_map.add((a, b))
1530
- coupling_map.add((b, a))
1531
1594
 
1532
1595
  self._coupling_map = sorted(coupling_map)
1533
1596
 
@@ -1541,11 +1604,9 @@ class QrackAceBackend:
1541
1604
  )
1542
1605
  noise_model = NoiseModel()
1543
1606
 
1607
+ # Single-qubit depolarizing only on boundary qubits
1544
1608
  boundary_qubits = set()
1545
1609
  for a, b in self.get_logical_coupling_map():
1546
- if a > b:
1547
- #Skip duplicates
1548
- continue
1549
1610
  col_a = a % self._row_length
1550
1611
  col_b = b % self._row_length
1551
1612
  is_long_a = self._is_col_long_range[col_a]
@@ -1556,6 +1617,12 @@ class QrackAceBackend:
1556
1617
  if not is_long_b:
1557
1618
  boundary_qubits.add(b)
1558
1619
 
1620
+ for q in boundary_qubits:
1621
+ for gate in ["u", "u1", "u2", "u3", "h", "x", "y", "z",
1622
+ "s", "sdg", "t", "tdg", "rx", "ry", "rz"]:
1623
+ noise_model.add_quantum_error(
1624
+ depolarizing_error(x, 1), gate, [q])
1625
+
1559
1626
  # Two-qubit depolarizing on boundary-crossing and boundary-adjacent gates
1560
1627
  for a, b in self.get_logical_coupling_map():
1561
1628
  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.2
3
+ Version: 2.2.4
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.2"
10
+ VERSION = "2.2.4"
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')