pyqrack-cpu-complex128 2.19.2__tar.gz → 2.20.0__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_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/Makefile +1 -1
  2. {pyqrack_cpu_complex128-2.19.2/pyqrack_cpu_complex128.egg-info → pyqrack_cpu_complex128-2.20.0}/PKG-INFO +1 -1
  3. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_ace_backend.py +452 -241
  4. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0/pyqrack_cpu_complex128.egg-info}/PKG-INFO +1 -1
  5. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/setup.py +1 -1
  6. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/LICENSE +0 -0
  7. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/MANIFEST.in +0 -0
  8. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/README.md +0 -0
  9. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyproject.toml +0 -0
  10. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/__init__.py +0 -0
  11. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/neuron_activation_fn.py +0 -0
  12. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/pauli.py +0 -0
  13. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_circuit.py +0 -0
  14. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  15. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_neuron.py +0 -0
  16. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  17. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_simulator.py +0 -0
  18. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_stabilizer.py +0 -0
  19. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_system/__init__.py +0 -0
  20. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_system/qrack_system.py +0 -0
  21. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/quimb_circuit_type.py +0 -0
  22. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/stats/__init__.py +0 -0
  23. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/stats/load_quantized_data.py +0 -0
  24. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack/stats/quantize_by_range.py +0 -0
  25. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/SOURCES.txt +0 -0
  26. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/dependency_links.txt +0 -0
  27. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/not-zip-safe +0 -0
  28. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/requires.txt +0 -0
  29. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/top_level.txt +0 -0
  30. {pyqrack_cpu_complex128-2.19.2 → pyqrack_cpu_complex128-2.20.0}/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 b51b54d8512cd3d57d0eb395556d40b14c35cc96; cd ..
33
+ git clone https://github.com/unitaryfund/qrack.git; cd qrack; git checkout 697ff96c730f27fa7d1ed4502b1044b13598ea1a; 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-cpu-complex128
3
- Version: 2.19.2
3
+ Version: 2.20.0
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -453,39 +453,97 @@ class QrackAceBackend:
453
453
  lq_idx = len(self._qubits)
454
454
  self._qubits.append([(sim_id, phys_idx)])
455
455
  self._detect_ancilla2_lq.append(lq_idx)
456
- # Boundary repetition code, on-demand design: only the couplers
457
- # are actually noisy here -- single-qubit gates are already
458
- # exactly transversal, per-replica, with zero error, so there's
459
- # no need to maintain a permanently-synced repetition code across
460
- # them at all. Instead: TWO dedicated, reusable scratch ancillae
461
- # per patch (separate from _detect_ancilla1/2 above, to avoid any
462
- # contention between this mechanism and that one within the same
463
- # coupling event), allocated ONCE, and reused across every
464
- # boundary qubit that patch ever protects -- not one allocation
465
- # per boundary qubit the way the earlier, always-on version did.
466
- # See _cpauli for the actual encode/couple/decode/correct cycle
467
- # built around these; this block is just the qubit-index
468
- # bookkeeping, in the same style as _detect_ancilla1/2 above.
469
- self._rep_code_ancilla1 = []
470
- self._rep_code_ancilla2 = []
456
+ # Boundary repetition code, PERMANENT design: with is_1d_chain
457
+ # and/or the "sacrifice one ideal qubit per patch to double
458
+ # patch count on the same hardware budget" tradeoff, patches
459
+ # are smaller and more numerous than when this was first tried,
460
+ # making permanently-held partners affordable in a way they
461
+ # weren't before -- this is the original proposal, revisited.
462
+ # Unlike the on-demand design just above/below this comment
463
+ # historically lived (fresh encode/extend/decode per coupling
464
+ # event), these partners are kept CONTINUOUSLY, transversally
465
+ # in sync with their boundary qubit: single-qubit gates are
466
+ # already exact, per-replica, so syncing partners through them
467
+ # costs nothing beyond the extra qubits themselves (see the
468
+ # _rep_code_apply_gate hook threaded through every transversal
469
+ # gate method below). Only the SYNDROME-CHECK ancilla is
470
+ # shared, one per patch, reused across every boundary qubit
471
+ # that patch protects -- not one dedicated ancilla per boundary
472
+ # qubit, which would scale with boundary-qubit count instead of
473
+ # patch count. Kept separate from _detect_ancilla1/2 above, to
474
+ # avoid any contention with the is_error_detection gadget
475
+ # within the same coupling event, since both are meant to work
476
+ # in concert, not compete for the same physical ancilla.
477
+ self._rep_code_syn_ancilla = []
471
478
  if self.is_boundary_repetition_code:
472
479
  for i in range(len(sim_counts)):
473
- self._rep_code_ancilla1.append(sim_counts[i])
474
- sim_counts[i] += 1
475
- self._rep_code_ancilla2.append(sim_counts[i])
480
+ self._rep_code_syn_ancilla.append(sim_counts[i])
476
481
  sim_counts[i] += 1
477
482
 
478
- self._rep_code_ancilla1_lq = []
479
- self._rep_code_ancilla2_lq = []
483
+ self._rep_code_syn_ancilla_lq = []
480
484
  if self.is_boundary_repetition_code:
481
- for sim_id, phys_idx in enumerate(self._rep_code_ancilla1):
482
- lq_idx = len(self._qubits)
483
- self._qubits.append([(sim_id, phys_idx)])
484
- self._rep_code_ancilla1_lq.append(lq_idx)
485
- for sim_id, phys_idx in enumerate(self._rep_code_ancilla2):
485
+ for sim_id, phys_idx in enumerate(self._rep_code_syn_ancilla):
486
486
  lq_idx = len(self._qubits)
487
487
  self._qubits.append([(sim_id, phys_idx)])
488
- self._rep_code_ancilla2_lq.append(lq_idx)
488
+ self._rep_code_syn_ancilla_lq.append(lq_idx)
489
+
490
+ # PERMANENT partners: one hidden, single-replica "ideal" qubit
491
+ # per non-crossbar patch each boundary qubit touches -- 2 for
492
+ # an edge qubit (every boundary qubit, when is_1d_chain=True,
493
+ # since a 1D chain has no row-direction structure and hence no
494
+ # corners at all), 4 for a 2D corner. The crossbar replica is
495
+ # excluded as a partner leg for the same reason as everywhere
496
+ # else in this file: it has no adjacent patch bulk of its own
497
+ # to pair with. Allocated by iterating self._qubits AFTER the
498
+ # ancilla allocation above, so appending mid-scan can't shift
499
+ # anything already recorded (range() below captures the
500
+ # original length once, before any of this loop's own
501
+ # appends -- a Python quirk relied on deliberately here, not
502
+ # accidentally).
503
+ self._rep_code_partners = {}
504
+ if self.is_boundary_repetition_code:
505
+ for lq in range(len(self._qubits)):
506
+ hq = self._qubits[lq]
507
+ if len(hq) < 2:
508
+ continue
509
+ partner_sims = []
510
+ for replica in hq:
511
+ if has_boundary and replica[0] == boundary_sim_id:
512
+ continue
513
+ partner_sims.append(replica[0])
514
+ resolved_partners = []
515
+ for sim_id in partner_sims:
516
+ idx = sim_counts[sim_id]
517
+ sim_counts[sim_id] += 1
518
+ p_lq = len(self._qubits)
519
+ self._qubits.append([(sim_id, idx)])
520
+ resolved_partners.append(p_lq)
521
+ self._rep_code_partners[lq] = resolved_partners
522
+
523
+ # Suppresses partner re-sync (via _rep_code_apply_gate) while a
524
+ # correction is itself in progress, applying x()/z() to lq
525
+ # directly -- otherwise that correction would recursively
526
+ # re-trigger a partner sync, undoing the very thing the
527
+ # correction just decided about the partners.
528
+ self._rep_code_correcting = False
529
+
530
+ # Suppresses the coupling-extension and syndrome-check logic in
531
+ # _cpauli specifically (NOT the transversal gate sync, which
532
+ # stays on) during cy()/cz()'s own internal H-sandwich. Found
533
+ # necessary directly, not preemptively: cz()'s CZ-symmetric
534
+ # direction choice can put a protected boundary qubit as the
535
+ # TARGET of the sandwich's inner cx() call, controlled by
536
+ # whatever the OTHER operand of the original cz() was -- not
537
+ # the user's original control at all -- while both the boundary
538
+ # qubit and its partners sit in the sandwich's temporarily
539
+ # H-rotated frame, where every reading is legitimately near 0.5.
540
+ # Checking syndrome agreement there compares two independently
541
+ # shadow-approximated near-0.5 values, which disagree often
542
+ # enough by pure chance to trigger real, wrong corrections; the
543
+ # sandwich's own outer H() calls already correctly sync
544
+ # partners transversally, so nothing is lost by skipping the
545
+ # coupling-extension/syndrome step for just this inner call.
546
+ self._rep_code_suppress_check = False
489
547
 
490
548
  # capture calls (see _apply_coupling): those calls go through
491
549
  # the ordinary _cpauli -> _apply_coupling path themselves, which
@@ -633,29 +691,22 @@ class QrackAceBackend:
633
691
  def _cz_shadow(self, q1, q2):
634
692
  p1 = self.sim[q1[0]].prob(q1[1]) if isinstance(q1, tuple) else q1.prob()
635
693
  p2 = self.sim[q2[0]].prob(q2[1]) if isinstance(q2, tuple) else q2.prob()
636
-
637
- # 0/1-BALANCE FIX: a near-tie (both readings within epsilon of each
638
- # other -- the ORDINARY case for a maximally-mixed control paired
639
- # with a freshly-H'd target, both landing at EXACTLY 0.5) carries
640
- # no informative signal about which basis state to commit to. The
641
- # previous logic computed a "prob_max" from this near-tie and
642
- # compared it against a ">= 0.5 - epsilon" threshold -- but
643
- # prob_max, drawn from two values both AT 0.5, ALWAYS satisfies
644
- # that threshold, so the Z gate fired deterministically, every
645
- # single time, regardless of the random draw upstream. Confirmed
646
- # empirically: 20/20 trials landed the shadow replica at exactly
647
- # prob=1.0, never 0.5 or anything reflecting genuine 50/50
648
- # uncertainty. A real CX from a mixed control onto a fresh target
649
- # leaves the target's own marginal at a genuine 0.5, not a
650
- # deterministic bias toward |1>.
651
- #
652
- # Fix: when the two readings are genuinely tied, the DECISION of
653
- # whether to apply Z must itself be the random draw (not merely
654
- # which value gets compared against the threshold, which never
655
- # changed the deterministic outcome). When the readings are
656
- # NOT tied, the existing decisive-threshold logic is unambiguous
657
- # and unchanged.
658
- if abs(p1 - p2) <= self._epsilon:
694
+ target = q2
695
+
696
+ # 0/1-BALANCE FIX: a near-tie carries no informative signal
697
+ # about which basis state to commit to. The correct test for
698
+ # that is whether EACH reading, individually, is itself near
699
+ # 0.5 -- not whether the two readings are near EACH OTHER.
700
+ # Comparing p1 to p2 directly was wrong: two readings can
701
+ # agree, decisively and exactly (e.g. two replicas genuinely
702
+ # tied to the same value, both confidently |1>), and that
703
+ # agreement is exactly the strongest, most reliable signal
704
+ # this shadow ever sees -- treating it as "no information" and
705
+ # coin-flipping anyway threw away the best case, not the worst
706
+ # one. Confirmed empirically: forcing random.random() and
707
+ # counting calls showed the coin-flip branch firing for
708
+ # p1=p2=1.0 exactly, which should be fully deterministic.
709
+ if (abs(p1 - 0.5) <= self._epsilon) and (abs(p2 - 0.5) <= self._epsilon):
659
710
  apply = random.random() < 0.5
660
711
  else:
661
712
  # Symmetric-in-control-vs-target CZ shadow, by design: pick
@@ -668,18 +719,71 @@ class QrackAceBackend:
668
719
  # control definitively |0> paired with a freshly-H'd target
669
720
  # sitting at p=0.5, where the old logic fired regardless of
670
721
  # the control's actual value.
722
+ #
723
+ # The Z then goes to whichever qubit was NOT read: applying
724
+ # it back onto the qubit whose own probability just decided
725
+ # the outcome is not what a genuinely symmetric CZ calls
726
+ # for. Confirmed empirically: with q1 mixed and q2 the
727
+ # decisive one, the Z landed nowhere at all under the old
728
+ # "always q2" rule, when it should have landed on q1.
671
729
  d1 = abs(p1 - 0.5)
672
730
  d2 = abs(p2 - 0.5)
673
- decisive = p1 if d1 >= d2 else p2
731
+ if d1 >= d2:
732
+ decisive = p1
733
+ else:
734
+ decisive = p2
735
+ target = q1
674
736
  apply = decisive >= (0.5 - self._epsilon)
675
737
 
676
- # The Z gate always targets q2 (the actual shadow target) --
677
- # never q1 (the control) -- per the earlier phase-kickback fix.
678
738
  if apply:
679
- if isinstance(q2, tuple):
680
- self.sim[q2[0]].z(q2[1])
739
+ if isinstance(target, tuple):
740
+ self.sim[target[0]].z(target[1])
741
+ else:
742
+ target.z()
743
+
744
+ def _shadow_control_apply(self, c, t, gate_name):
745
+ """Non-symmetric, control-only shadow: the decision is based
746
+ ONLY on the control's own probability, never on whichever of
747
+ control/target happens to be more polarized, and the payload
748
+ gate (gate_name -- "x" for CX, "y" for CY) is applied directly
749
+ to the target with NO conjugation at all.
750
+
751
+ This is the hot path for CX and CY -- NOT for a genuinely,
752
+ directly requested CZ, which keeps using the fully symmetric
753
+ _cz_shadow above, unchanged, and NOT for CZ's own "hot path"
754
+ reformulation in cz() either, which keeps its dynamic, CZ-
755
+ symmetric choice of which side to wrap (see cz()).
756
+
757
+ The earlier design conjugated the target with H (for CX) or S
758
+ (for CY, itself built from an H-conjugated CX) specifically so
759
+ the SAME symmetric CZ logic could serve every gate type by
760
+ letting the shadow read either side. Once the decision is
761
+ control-only (see the CX/CZ-symmetry fix above), that
762
+ conjugation no longer accomplishes anything: H.Z.H = X and
763
+ S.X.Sdag = Y exactly (verified directly), and the identity
764
+ no-fire case collapses to a genuine no-op either way. Applying
765
+ the payload gate directly, only when warranted, skips two now-
766
+ pointless gate calls per shadow decision on what is likely the
767
+ hottest path in the whole architecture -- CX is typically the
768
+ most common two-qubit gate, and CY/CZ often reduce to it here
769
+ anyway.
770
+ """
771
+ p = self.sim[c[0]].prob(c[1]) if isinstance(c, tuple) else c.prob()
772
+
773
+ # Same "genuine uncertainty gets a genuine coin flip" principle
774
+ # as _cz_shadow above, just against the control's own distance
775
+ # from 0.5 rather than a control-vs-target comparison, since
776
+ # there's only one reading in play here.
777
+ if abs(p - 0.5) <= self._epsilon:
778
+ apply = random.random() < 0.5
779
+ else:
780
+ apply = p > 0.5
781
+
782
+ if apply:
783
+ if isinstance(t, tuple):
784
+ getattr(self.sim[t[0]], gate_name)(t[1])
681
785
  else:
682
- q2.z()
786
+ getattr(t, gate_name)()
683
787
 
684
788
  def _qec_x(self, c):
685
789
  if isinstance(c, tuple):
@@ -687,24 +791,6 @@ class QrackAceBackend:
687
791
  else:
688
792
  c.x()
689
793
 
690
- def _qec_h(self, t):
691
- if isinstance(t, tuple):
692
- self.sim[t[0]].h(t[1])
693
- else:
694
- t.h()
695
-
696
- def _qec_s(self, t):
697
- if isinstance(t, tuple):
698
- self.sim[t[0]].s(t[1])
699
- else:
700
- t.s()
701
-
702
- def _qec_adjs(self, t):
703
- if isinstance(t, tuple):
704
- self.sim[t[0]].adjs(t[1])
705
- else:
706
- t.adjs()
707
-
708
794
  def _detect_swap(self, sim_id, idx1, idx2):
709
795
  self.sim[sim_id].swap(idx1, idx2)
710
796
 
@@ -717,17 +803,13 @@ class QrackAceBackend:
717
803
  self._anti_shadow_wrap(c, t, self._cz_shadow)
718
804
 
719
805
  def _cx_shadow(self, c, t):
720
- self._qec_h(t)
721
- self._cz_shadow(c, t)
722
- self._qec_h(t)
806
+ self._shadow_control_apply(c, t, "x")
723
807
 
724
808
  def _anti_cx_shadow(self, c, t):
725
809
  self._anti_shadow_wrap(c, t, self._cx_shadow)
726
810
 
727
811
  def _cy_shadow(self, c, t):
728
- self._qec_adjs(t)
729
- self._cx_shadow(c, t)
730
- self._qec_s(t)
812
+ self._shadow_control_apply(c, t, "y")
731
813
 
732
814
  def _anti_cy_shadow(self, c, t):
733
815
  self._anti_shadow_wrap(c, t, self._cy_shadow)
@@ -1091,6 +1173,26 @@ class QrackAceBackend:
1091
1173
  math.atan(math.tan(i) * b) - i,
1092
1174
  )
1093
1175
 
1176
+ def _rep_code_apply_gate(self, lq, gate_name, *args):
1177
+ """Apply the same single-qubit gate, with the same arguments, to
1178
+ every permanent repetition-code partner of lq -- keeps partners
1179
+ continuously, transversally in sync with their boundary qubit.
1180
+ Called from the tail of every transversal single-qubit gate
1181
+ method below. Safe to call unconditionally: a no-op whenever
1182
+ the feature is off, lq has no partners (bulk qubits), or a
1183
+ correction is currently in progress (see _rep_code_correcting),
1184
+ since that correction's own x()/z() call on lq would otherwise
1185
+ recursively re-trigger a partner sync mid-correction.
1186
+ """
1187
+ if not self.is_boundary_repetition_code or self._rep_code_correcting:
1188
+ return
1189
+ partners = self._rep_code_partners.get(lq)
1190
+ if not partners:
1191
+ return
1192
+ for p_lq in partners:
1193
+ sim_id, idx = self._qubits[p_lq][0]
1194
+ getattr(self.sim[sim_id], gate_name)(idx, *args)
1195
+
1094
1196
  def u(self, lq, th, ph, lm):
1095
1197
  hq = self._unpack(lq)
1096
1198
  if len(hq) < 2:
@@ -1111,6 +1213,8 @@ class QrackAceBackend:
1111
1213
  if lhv is not None:
1112
1214
  lhv.u(th, ph, lm)
1113
1215
 
1216
+ self._rep_code_apply_gate(lq, "u", th, ph, lm)
1217
+
1114
1218
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
1115
1219
 
1116
1220
  def _revert_shadow_commitment(self, lq1):
@@ -1204,6 +1308,11 @@ class QrackAceBackend:
1204
1308
  elif p == Pauli.PauliZ:
1205
1309
  lhv.rz(th)
1206
1310
 
1311
+ if self.is_boundary_repetition_code and not self._rep_code_correcting:
1312
+ for p_lq in self._rep_code_partners.get(lq, ()):
1313
+ sim_id, idx = self._qubits[p_lq][0]
1314
+ self.sim[sim_id].r(p, th, idx) # r()'s idx is LAST, unlike every other gate
1315
+
1207
1316
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
1208
1317
 
1209
1318
  def h(self, lq):
@@ -1226,6 +1335,8 @@ class QrackAceBackend:
1226
1335
  if lhv is not None:
1227
1336
  lhv.h()
1228
1337
 
1338
+ self._rep_code_apply_gate(lq, "h")
1339
+
1229
1340
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
1230
1341
 
1231
1342
  def s(self, lq):
@@ -1248,6 +1359,8 @@ class QrackAceBackend:
1248
1359
  if lhv is not None:
1249
1360
  lhv.s()
1250
1361
 
1362
+ self._rep_code_apply_gate(lq, "s")
1363
+
1251
1364
  def adjs(self, lq):
1252
1365
  hq = self._unpack(lq)
1253
1366
  if len(hq) < 2:
@@ -1268,6 +1381,8 @@ class QrackAceBackend:
1268
1381
  if lhv is not None:
1269
1382
  lhv.adjs()
1270
1383
 
1384
+ self._rep_code_apply_gate(lq, "adjs")
1385
+
1271
1386
  def sx(self, lq):
1272
1387
  hq = self._unpack(lq)
1273
1388
  if len(hq) < 2:
@@ -1288,6 +1403,8 @@ class QrackAceBackend:
1288
1403
  if lhv is not None:
1289
1404
  lhv.sx()
1290
1405
 
1406
+ self._rep_code_apply_gate(lq, "sx")
1407
+
1291
1408
  def adjsx(self, lq):
1292
1409
  hq = self._unpack(lq)
1293
1410
  if len(hq) < 2:
@@ -1308,129 +1425,147 @@ class QrackAceBackend:
1308
1425
  if lhv is not None:
1309
1426
  lhv.adjsx()
1310
1427
 
1311
- def _rep_code_patch_triples(self, lq):
1312
- """(patch_replica, ancilla1_lq, ancilla2_lq) triples for lq's
1313
- non-crossbar patches -- one per patch this boundary qubit
1314
- touches. Recomputed fresh on every call (this is the on-demand
1315
- design: nothing persists between coupling events), rather than
1316
- stored once at construction the way the earlier, always-on
1317
- version did. Empty if lq is bulk (single-replica) or the
1318
- feature is off -- both cases where there's nothing to protect
1319
- or nowhere to protect it.
1428
+ self._rep_code_apply_gate(lq, "adjsx")
1429
+
1430
+ def _rep_code_patch_pairs(self, lq):
1431
+ """(patch_replica, partner_lq, syn_ancilla_lq) triples for lq --
1432
+ one per PERMANENT partner (see the constructor allocation),
1433
+ i.e. one per non-crossbar patch lq touches: 2 for an edge
1434
+ qubit, 4 for a 2D corner. Empty if lq is bulk (single-replica)
1435
+ or the feature is off.
1320
1436
  """
1321
1437
  if not self.is_boundary_repetition_code:
1322
1438
  return []
1323
- hq = self._qubits[lq]
1324
- if len(hq) < 2:
1439
+ partners = self._rep_code_partners.get(lq)
1440
+ if not partners:
1325
1441
  return []
1326
- triples = []
1442
+ hq = self._qubits[lq]
1443
+ pairs = []
1444
+ p_i = 0
1327
1445
  for replica in hq:
1328
1446
  if self._boundary_sim_id is not None and replica[0] == self._boundary_sim_id:
1329
1447
  continue
1330
- sim_id = replica[0]
1331
- triples.append((replica, self._rep_code_ancilla1_lq[sim_id], self._rep_code_ancilla2_lq[sim_id]))
1332
- return triples
1333
-
1334
- def _rep_code_encode(self, lq):
1335
- """Step 1: CNOT lq's replica-in-each-patch onto that patch's
1336
- dedicated, reusable ancilla1 -- a fresh, TEMPORARY repetition-
1337
- code copy, good for exactly the duration of one coupling event.
1338
- Returns the triples for the caller to pass to
1339
- _rep_code_extend_coupling and _rep_code_decode_and_correct, or
1340
- an empty list if lq isn't repetition-code eligible right now.
1448
+ pairs.append((replica, partners[p_i], self._rep_code_syn_ancilla_lq[replica[0]]))
1449
+ p_i += 1
1450
+ return pairs
1451
+
1452
+ def _rep_code_extend_coupling(self, pauli, anti, qb1, hq1, lq1_lr, lq1, pairs):
1453
+ """Extends the SAME coupling (same control) to each PERMANENT
1454
+ partner -- reuses _apply_coupling directly, the same real/
1455
+ shadow dispatch every other target already gets. Only
1456
+ meaningful for a TARGET's pairs: a CONTROL's own value is
1457
+ never touched by gate_fn/shadow_fn in the first place, so
1458
+ there's nothing for a control's partners to need extended to.
1341
1459
  """
1342
- triples = self._rep_code_patch_triples(lq)
1343
- for (sim_id, b_idx), anc1_lq, _ in triples:
1344
- anc1_idx = self._qubits[anc1_lq][0][1]
1345
- self.sim[sim_id].mcx([b_idx], anc1_idx)
1346
- return triples
1347
-
1348
- def _rep_code_extend_coupling(self, pauli, anti, qb1, hq1, lq1_lr, lq1, triples):
1349
- """Step 2: extend the SAME coupling (same control) to each
1350
- freshly-encoded ancilla1 -- reuses _apply_coupling directly,
1351
- the same real/shadow dispatch every other target already gets.
1352
- Only meaningful for a TARGET's triples: a CONTROL's own value
1353
- is never touched by gate_fn/shadow_fn in the first place (the
1354
- established, control-invariance fact this whole file already
1355
- relies on), so there's nothing for a control's ancilla1 to need
1356
- extended to -- callers only pass a target's triples here.
1357
- """
1358
- for _, anc1_lq, _ in triples:
1359
- hq_a = self._qubits[anc1_lq]
1360
- qb_a, _ = QrackAceBackend._get_qb_lhv_indices(hq_a)
1361
- self._apply_coupling(pauli, anti, qb1, hq1, qb_a, hq_a, lq1_lr, lq1, anc1_lq)
1362
-
1363
- def _rep_code_decode_and_correct(self, lq, triples, phase=False):
1364
- """Steps 3-5: bit-flip syndrome-check-and-correct (using
1365
- ancilla2 per patch), THEN the final decode CNOT (lq's replica
1366
- -> ancilla1 again), which -- once any disagreement has already
1367
- been corrected -- returns ancilla1 exactly to |0>, ready for
1368
- the next boundary qubit's coupling event to reuse. An explicit
1369
- measure-and-reset backstops the decode CNOT for the rare
1370
- (corner-only) multi-disagreement case left uncorrected below,
1371
- so ancilla1 is unconditionally clean afterward either way.
1460
+ for _, p_lq, _ in pairs:
1461
+ hq_p = self._qubits[p_lq]
1462
+ qb_p, _ = QrackAceBackend._get_qb_lhv_indices(hq_p)
1463
+ self._apply_coupling(pauli, anti, qb1, hq1, qb_p, hq_p, lq1_lr, lq1, p_lq)
1464
+
1465
+ def _rep_code_syndrome_correct(self, lq, pairs, phase=False):
1466
+ """Syndrome-check-and-correct against lq's PERMANENT partners,
1467
+ using the shared, per-patch syndrome ancilla. Unlike the
1468
+ earlier on-demand design, there is no encode/decode step here:
1469
+ the partners are already continuously, transversally in sync
1470
+ with lq (see _rep_code_apply_gate), so this only needs to
1471
+ check whether the coupling that just touched lq landed
1472
+ consistently on lq and every partner, and reset the shared
1473
+ syndrome ancilla for the next boundary qubit to reuse -- the
1474
+ partners themselves are never reset, since they're permanent.
1372
1475
 
1373
1476
  Same "try to force error-free, fall back to an unforced Born-
1374
1477
  rule read, then it's obvious by elimination" decoding pattern
1375
- used elsewhere in this file. With N syndrome bits (2 for an
1376
- edge qubit's 3-qubit code, 4 for a corner's 5-qubit code): zero
1377
- disagreeing patches means no error; exactly one unambiguously
1378
- identifies that patch's ancilla1 as the outlier (every other
1379
- patch's ancilla1, by construction, still agrees with lq); a
1380
- strict majority (including unanimity) unambiguously identifies
1381
- lq itself as the outlier. A strict minority greater than one is
1382
- not explainable by any single-qubit error and is left
1383
- uncorrected -- guessing would risk introducing a wrong
1384
- correction where an unrelated, uncorrectable multi-qubit event
1385
- happened instead.
1478
+ used elsewhere in this file: read prob() first; force_m to
1479
+ whichever branch is already within _ps_epsilon of certain;
1480
+ only fall through to a genuine, unforced measurement when
1481
+ neither branch is that confident. With N syndrome bits (2 for
1482
+ an edge qubit, 4 for a corner): zero disagreeing partners means
1483
+ no error; exactly one disagreeing partner unambiguously
1484
+ identifies that partner as the error; a strict majority
1485
+ (including unanimity) identifies lq itself as the outlier. A
1486
+ strict minority greater than one is not explainable by any
1487
+ single-qubit error and is left uncorrected.
1386
1488
  """
1387
- if not triples:
1489
+ if not pairs:
1388
1490
  return
1389
1491
 
1390
1492
  syndrome = []
1391
- for (sim_id, b_idx), anc1_lq, anc2_lq in triples:
1493
+ for (sim_id, b_idx), p_lq, syn_lq in pairs:
1392
1494
  sim = self.sim[sim_id]
1393
- anc1_idx = self._qubits[anc1_lq][0][1]
1394
- anc2_idx = self._qubits[anc2_lq][0][1]
1395
-
1396
- sim.mcx([b_idx], anc2_idx)
1397
- sim.mcx([anc1_idx], anc2_idx)
1398
-
1399
- p = sim.prob(anc2_idx)
1400
- bit = sim.m(anc2_idx)
1495
+ p_idx = self._qubits[p_lq][0][1]
1496
+ syn_idx = self._qubits[syn_lq][0][1]
1497
+
1498
+ if phase:
1499
+ sim.h(b_idx)
1500
+ sim.h(p_idx)
1501
+ sim.mcx([b_idx], syn_idx)
1502
+ sim.mcx([p_idx], syn_idx)
1503
+ if phase:
1504
+ sim.h(b_idx)
1505
+ sim.h(p_idx)
1506
+
1507
+ p = sim.prob(syn_idx)
1508
+ if p < self._ps_epsilon:
1509
+ sim.force_m(syn_idx, False)
1510
+ bit = False
1511
+ elif p >= (1.0 - self._ps_epsilon):
1512
+ sim.force_m(syn_idx, True)
1513
+ bit = True
1514
+ else:
1515
+ bit = sim.m(syn_idx)
1401
1516
  if bit:
1402
- sim.x(anc2_idx) # reset ancilla2 to |0> for reuse
1517
+ sim.x(syn_idx) # reset the shared syndrome ancilla for reuse
1403
1518
 
1404
- syndrome.append((bit, sim_id, anc1_idx))
1519
+ syndrome.append((bit, sim_id, p_idx))
1405
1520
 
1406
1521
  n = len(syndrome)
1407
1522
  disagree = sum(1 for bit, _, _ in syndrome if bit)
1408
- if disagree > 0:
1409
- if disagree > n - disagree:
1410
- # Flip all bits
1523
+ if disagree == 0:
1524
+ return
1525
+
1526
+ if disagree > n - disagree:
1527
+ # Strict majority (including unanimity): lq itself is the
1528
+ # outlier. Correct via the ordinary logical gate, so it
1529
+ # transversally touches every one of lq's own replicas the
1530
+ # way any other x()/z() call would -- suppressing the
1531
+ # partner re-sync (see _rep_code_apply_gate) since the
1532
+ # partners were just independently verified correct.
1533
+ self._rep_code_correcting = True
1534
+ try:
1411
1535
  if phase:
1412
1536
  self.z(lq)
1413
1537
  else:
1414
1538
  self.x(lq)
1415
- # Necessary either way, even if all flipped
1416
- for bit, sim_id, anc1_idx in syndrome:
1539
+ finally:
1540
+ self._rep_code_correcting = False
1541
+ elif disagree == 1:
1542
+ # exactly one partner disagrees -- unambiguously that partner
1543
+ for bit, sim_id, p_idx in syndrome:
1417
1544
  if bit:
1418
1545
  if phase:
1419
- self.sim[sim_id].z(anc1_idx)
1546
+ self.sim[sim_id].z(p_idx)
1420
1547
  else:
1421
- self.sim[sim_id].x(anc1_idx)
1422
-
1423
- for (sim_id, b_idx), anc1_lq, _ in triples:
1424
- anc1_idx = self._qubits[anc1_lq][0][1]
1425
- sim = self.sim[sim_id]
1426
- sim.mcx([b_idx], anc1_idx)
1427
- p = sim.prob(anc1_idx)
1428
- if self._ps_epsilon >= (1.0 - p):
1429
- b = sim.m(anc1_idx)
1430
- else:
1431
- b = sim.force_m(anc1_idx, False)
1432
- if b:
1433
- sim.x(anc1_idx)
1548
+ self.sim[sim_id].x(p_idx)
1549
+ # else: strict minority > 1 (only possible with the corner
1550
+ # code) -- not explainable by any single-qubit error, left
1551
+ # uncorrected rather than guessed at.
1552
+
1553
+ def _rep_code_check_pair(self, lq1, lq2):
1554
+ """Runs the syndrome check on both operands directly, bypassing
1555
+ _cpauli entirely. Used after cy()/cz()/acy()/acz()'s H-sandwich
1556
+ fully unwinds, to make up for _rep_code_suppress_check having
1557
+ skipped the check during the sandwich's own internal coupling
1558
+ call -- the check needs to happen exactly once, after the
1559
+ user's full requested operation, not mid-sandwich (see
1560
+ _rep_code_suppress_check for why mid-sandwich specifically goes
1561
+ wrong) and not zero times either.
1562
+ """
1563
+ if not self.is_boundary_repetition_code:
1564
+ return
1565
+ pairs1 = self._rep_code_patch_pairs(lq1)
1566
+ self._rep_code_syndrome_correct(lq1, pairs1)
1567
+ pairs2 = self._rep_code_patch_pairs(lq2)
1568
+ self._rep_code_syndrome_correct(lq2, pairs2)
1434
1569
 
1435
1570
  def x(self, lq):
1436
1571
  hq = self._unpack(lq)
@@ -1452,6 +1587,8 @@ class QrackAceBackend:
1452
1587
  if lhv is not None:
1453
1588
  lhv.x()
1454
1589
 
1590
+ self._rep_code_apply_gate(lq, "x")
1591
+
1455
1592
  def y(self, lq):
1456
1593
  hq = self._unpack(lq)
1457
1594
  if len(hq) < 2:
@@ -1472,6 +1609,8 @@ class QrackAceBackend:
1472
1609
  if lhv is not None:
1473
1610
  lhv.y()
1474
1611
 
1612
+ self._rep_code_apply_gate(lq, "y")
1613
+
1475
1614
  def z(self, lq):
1476
1615
  hq = self._unpack(lq)
1477
1616
  if len(hq) < 2:
@@ -1492,6 +1631,8 @@ class QrackAceBackend:
1492
1631
  if lhv is not None:
1493
1632
  lhv.z()
1494
1633
 
1634
+ self._rep_code_apply_gate(lq, "z")
1635
+
1495
1636
  def t(self, lq):
1496
1637
  hq = self._unpack(lq)
1497
1638
  if len(hq) < 2:
@@ -1512,6 +1653,8 @@ class QrackAceBackend:
1512
1653
  if lhv is not None:
1513
1654
  lhv.t()
1514
1655
 
1656
+ self._rep_code_apply_gate(lq, "t")
1657
+
1515
1658
  def adjt(self, lq):
1516
1659
  hq = self._unpack(lq)
1517
1660
  if len(hq) < 2:
@@ -1532,6 +1675,8 @@ class QrackAceBackend:
1532
1675
  if lhv is not None:
1533
1676
  lhv.adjt()
1534
1677
 
1678
+ self._rep_code_apply_gate(lq, "adjt")
1679
+
1535
1680
  def _get_gate(self, pauli, anti, sim_id):
1536
1681
  gate = None
1537
1682
  shadow = None
@@ -1772,40 +1917,34 @@ class QrackAceBackend:
1772
1917
  self._correct(lq1)
1773
1918
  self._correct(lq2)
1774
1919
 
1775
- # Boundary repetition code, on-demand: encode/couple/decode/
1776
- # correct wraps ONLY the actual coupler call immediately below,
1777
- # tightly -- not the transversal single-qubit gates (already
1778
- # exact, per-replica, nothing to protect), and not the outer
1779
- # cy()/cz() wrapper when this _cpauli call is their internal,
1780
- # H-sandwiched cx() (see below for why that's actually fine now,
1781
- # unlike the earlier design). Guarded by not
1782
- # self._in_gadget_capture the same way the _correct() calls
1783
- # above are: a nested capture's own target is always a hidden,
1784
- # single-replica ancilla (never boundary-eligible, so this would
1785
- # no-op anyway), but the CONTROL side could still be a protected
1786
- # boundary qubit, and re-running its own encode/check/decode
1787
- # cycle on every nested capture would be wasteful and redundant
1788
- # with the outer call already doing it once.
1920
+ # Boundary repetition code, PERMANENT partners: unlike the
1921
+ # earlier on-demand design (fresh encode/decode wrapped tightly
1922
+ # around just the coupler), partners here are ALREADY,
1923
+ # continuously kept in sync via every transversal gate (see
1924
+ # _rep_code_apply_gate) -- so there's no encode step needed at
1925
+ # all, just: (1) verify/correct lq1's reliability right before
1926
+ # the coupler reads it, since the control is never gated by
1927
+ # gate_fn/shadow_fn itself but its CURRENT value still needs to
1928
+ # be trustworthy; (2) extend the coupler's effect on lq2 to its
1929
+ # partners too, then check the result.
1789
1930
  #
1790
- # lq1 (control) gets a SIMPLER cycle than lq2 (target): the
1791
- # coupler never gates the control at all (the same control-
1792
- # invariance fact this whole file already relies on), so there's
1793
- # no coupling effect to extend to its ancilla1 -- just verify/
1794
- # correct lq1's OWN reliability once, right before the coupler
1795
- # is about to read it, so a stale value doesn't feed a wrong
1796
- # shadow decision.
1797
- if self.is_boundary_repetition_code and not self._in_gadget_capture:
1798
- t1 = self._rep_code_encode(lq1)
1799
- self._rep_code_decode_and_correct(lq1, t1)
1800
-
1801
- # lq2 (target) encode happens BEFORE the coupler, so the SAME
1802
- # coupling can be extended to ancilla1 right after -- capturing
1803
- # a "before" snapshot the coupler's own effect then lands on
1804
- # too, the same real/shadow dispatch every other target gets.
1805
- t2 = []
1806
- if self.is_boundary_repetition_code and not self._in_gadget_capture:
1807
- t2 = self._rep_code_encode(lq2)
1808
- if t2:
1931
+ # This is safe even mid-H-sandwich (cy()/cz()'s internal
1932
+ # h();cx();h(), which reaches this same _cpauli call) for a
1933
+ # different reason than the on-demand design's safety: partners
1934
+ # receive every transversal gate lq2 does, INCLUDING that
1935
+ # sandwich's own h() calls, so by the time this inner coupling
1936
+ # runs, lq2 and its partners are already, consistently, TOGETHER
1937
+ # in whatever frame the sandwich has rotated into -- the
1938
+ # comparison never spans a rotation either side of it missed.
1939
+ # Verified directly against exactly this scenario.
1940
+ if self.is_boundary_repetition_code and not self._in_gadget_capture and not self._rep_code_suppress_check:
1941
+ pairs1 = self._rep_code_patch_pairs(lq1)
1942
+ self._rep_code_syndrome_correct(lq1, pairs1)
1943
+
1944
+ pairs2 = []
1945
+ if self.is_boundary_repetition_code and not self._in_gadget_capture and not self._rep_code_suppress_check:
1946
+ pairs2 = self._rep_code_patch_pairs(lq2)
1947
+ if pairs2:
1809
1948
  # Only defined for "patch-local" connectivity, the only
1810
1949
  # officially supported connectivity model here (get_
1811
1950
  # logical_coupling_map() only ever returns pairs that
@@ -1813,7 +1952,7 @@ class QrackAceBackend:
1813
1952
  # share NO simulator at all) only exists as a fallback
1814
1953
  # for users who ignore that map, and this repetition-
1815
1954
  # code layer doesn't try to support it: raise plainly
1816
- # instead of silently coupling ancilla1 through an
1955
+ # instead of silently coupling partners through an
1817
1956
  # undefined/unsupported path.
1818
1957
  sims1 = {b[0] for b in hq1}
1819
1958
  sims2 = {b[0] for b in hq2}
@@ -1831,8 +1970,8 @@ class QrackAceBackend:
1831
1970
  # qubits, which now live as real qubits in the shared boundary sim.
1832
1971
  self._apply_coupling(pauli, anti, qb1, hq1, qb2, hq2, lq1_lr, lq1, lq2)
1833
1972
 
1834
- if t2:
1835
- self._rep_code_extend_coupling(pauli, anti, qb1, hq1, lq1_lr, lq1, t2)
1973
+ if pairs2:
1974
+ self._rep_code_extend_coupling(pauli, anti, qb1, hq1, lq1_lr, lq1, pairs2)
1836
1975
 
1837
1976
  if lq2 in self._lhv:
1838
1977
  ctrl_prob = self.sim[hq1[0][0]].prob(hq1[0][1])
@@ -1841,21 +1980,16 @@ class QrackAceBackend:
1841
1980
  if self._in_gadget_capture:
1842
1981
  return
1843
1982
 
1844
- # Repetition-code decode-and-correct for lq2 happens HERE,
1983
+ # Repetition-code syndrome-check for lq2 happens HERE,
1845
1984
  # immediately after the coupler+extension, in the SAME frame
1846
1985
  # they just ran in -- deliberately BEFORE _correct()'s own,
1847
1986
  # separate soft reconciliation below, which can itself rotate
1848
- # lq2 (the phase=True call) and would otherwise pull it out of
1849
- # sync with an ancilla1 that never gets that same rotation. This
1850
- # is also exactly why cy()/cz()'s internal H-sandwich is safe
1851
- # now, unlike the earlier, always-on design: encode, couple,
1852
- # and this decode-and-correct are all contained within this one
1853
- # _cpauli call, whatever frame it happens to be running in --
1854
- # never spanning an external rotation the way permanently-
1855
- # synced partners, checked from a separate top-level call, used
1856
- # to.
1857
- if self.is_boundary_repetition_code:
1858
- self._rep_code_decode_and_correct(lq2, t2)
1987
+ # lq2 (the phase=True call) and would pull it out of sync with
1988
+ # partners that don't get that same call routed to them
1989
+ # (partners only track lq2's OWN transversal gates, not
1990
+ # _correct()'s internal bookkeeping).
1991
+ if self.is_boundary_repetition_code and not self._rep_code_suppress_check:
1992
+ self._rep_code_syndrome_correct(lq2, pairs2)
1859
1993
 
1860
1994
  # No post-coupling correction on lq1 (the control): _cz_shadow only
1861
1995
  # ever calls .z() on the target, never gates the control -- only
@@ -1876,16 +2010,74 @@ class QrackAceBackend:
1876
2010
  def cy(self, lq1, lq2):
1877
2011
  if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1878
2012
  self.adjs(lq2)
1879
- self.cx(lq1, lq2)
2013
+ self._rep_code_suppress_check = True
2014
+ try:
2015
+ self.cx(lq1, lq2)
2016
+ finally:
2017
+ self._rep_code_suppress_check = False
1880
2018
  self.s(lq2)
2019
+ self._rep_code_check_pair(lq1, lq2)
1881
2020
  else:
1882
2021
  self._cpauli(lq1, lq2, False, Pauli.PauliY)
1883
2022
 
1884
2023
  def cz(self, lq1, lq2):
1885
2024
  if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1886
- self.h(lq2)
1887
- self.cx(lq1, lq2)
1888
- self.h(lq2)
2025
+ # CZ's own exchange symmetry (CZ(a,b)=CZ(b,a)) means either
2026
+ # H-sandwich direction below gives the IDENTICAL, correct
2027
+ # CZ unitary -- so pick whichever leaves the underlying
2028
+ # cx()'s shadow reading the more decisively polarized
2029
+ # qubit, the same principle _cz_shadow itself already
2030
+ # applies directly for a genuinely, directly requested CZ
2031
+ # (the "else" branch below). This is the "hot path" fix:
2032
+ # without it, this branch (is_error_detection AND either
2033
+ # side boundary -- the common, practical case) always read
2034
+ # lq1 regardless of which side was actually more decisive,
2035
+ # since _cx_shadow's control-only basis (see
2036
+ # _shadow_control_apply) no longer inherits _cz_shadow's own
2037
+ # symmetric picking the way it used to.
2038
+ #
2039
+ # Does NOT extend to acz() below: anti-control isn't
2040
+ # symmetric the same way CZ is -- verified directly,
2041
+ # X-conjugating the control vs the target gives two
2042
+ # different gate matrices, not the same one -- so acz() has
2043
+ # no analogous second choice to pick between; lq1 must stay
2044
+ # the one read, since it's specifically the anti-controlled
2045
+ # qubit by the gate's own definition.
2046
+ #
2047
+ # The inner cx() here runs with the repetition code's
2048
+ # coupling-extension/syndrome-check suppressed (see
2049
+ # _rep_code_suppress_check): found directly, not
2050
+ # preemptively, that this symmetric direction choice can
2051
+ # put a protected boundary qubit as the inner cx()'s
2052
+ # TARGET, controlled by the OTHER operand of this cz() call
2053
+ # rather than the user's original control, while both sit
2054
+ # in the sandwich's temporarily H-rotated frame where every
2055
+ # reading is legitimately near 0.5 -- checking syndrome
2056
+ # agreement there compares two independently
2057
+ # shadow-approximated near-0.5 values, which disagreed
2058
+ # often enough by pure chance to trigger real, wrong
2059
+ # corrections. The transversal H() calls below still
2060
+ # correctly sync partners either way; _rep_code_check_pair
2061
+ # after the sandwich fully unwinds makes up for the
2062
+ # suppressed check, running exactly once instead of zero
2063
+ # times or mid-rotation.
2064
+ if abs(self.prob(lq1) - 0.5) >= abs(self.prob(lq2) - 0.5):
2065
+ self.h(lq2)
2066
+ self._rep_code_suppress_check = True
2067
+ try:
2068
+ self.cx(lq1, lq2)
2069
+ finally:
2070
+ self._rep_code_suppress_check = False
2071
+ self.h(lq2)
2072
+ else:
2073
+ self.h(lq1)
2074
+ self._rep_code_suppress_check = True
2075
+ try:
2076
+ self.cx(lq2, lq1)
2077
+ finally:
2078
+ self._rep_code_suppress_check = False
2079
+ self.h(lq1)
2080
+ self._rep_code_check_pair(lq1, lq2)
1889
2081
  else:
1890
2082
  self._cpauli(lq1, lq2, False, Pauli.PauliZ)
1891
2083
 
@@ -1895,16 +2087,26 @@ class QrackAceBackend:
1895
2087
  def acy(self, lq1, lq2):
1896
2088
  if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1897
2089
  self.adjs(lq2)
1898
- self.acx(lq1, lq2)
2090
+ self._rep_code_suppress_check = True
2091
+ try:
2092
+ self.acx(lq1, lq2)
2093
+ finally:
2094
+ self._rep_code_suppress_check = False
1899
2095
  self.s(lq2)
2096
+ self._rep_code_check_pair(lq1, lq2)
1900
2097
  else:
1901
2098
  self._cpauli(lq1, lq2, True, Pauli.PauliY)
1902
2099
 
1903
2100
  def acz(self, lq1, lq2):
1904
2101
  if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1905
2102
  self.h(lq2)
1906
- self.acx(lq1, lq2)
2103
+ self._rep_code_suppress_check = True
2104
+ try:
2105
+ self.acx(lq1, lq2)
2106
+ finally:
2107
+ self._rep_code_suppress_check = False
1907
2108
  self.h(lq2)
2109
+ self._rep_code_check_pair(lq1, lq2)
1908
2110
  else:
1909
2111
  self._cpauli(lq1, lq2, True, Pauli.PauliZ)
1910
2112
 
@@ -2003,13 +2205,22 @@ class QrackAceBackend:
2003
2205
  if lhv1 is not None:
2004
2206
  self._lhv[lq2] = lhv1
2005
2207
 
2006
- # Boundary repetition code needs no bookkeeping here, unlike
2007
- # self._qubits/self._lhv just above: it's on-demand now, not
2008
- # a permanent, per-logical-index dict of pre-allocated
2009
- # partners -- eligibility is just len(self._qubits[lq]) > 1,
2010
- # computed fresh on every coupling event, so the reference
2011
- # swap of self._qubits above already, automatically carries
2012
- # the right eligibility with it. Nothing else to move.
2208
+ # Boundary repetition code: partners are tied to a PHYSICAL
2209
+ # patch location, allocated once at construction and never
2210
+ # moved -- exactly like self._qubits and self._lhv just
2211
+ # above, which this mirrors. After swapping WHICH logical
2212
+ # index points to which physical replica list, the partner
2213
+ # mapping needs the same treatment, or lq2 (now pointing at
2214
+ # lq1's old physical hardware) would keep lq2's OLD
2215
+ # partners, checked against physically the wrong patches
2216
+ # from then on.
2217
+ if self.is_boundary_repetition_code:
2218
+ p1 = self._rep_code_partners.pop(lq1, None)
2219
+ p2 = self._rep_code_partners.pop(lq2, None)
2220
+ if p2 is not None:
2221
+ self._rep_code_partners[lq1] = p2
2222
+ if p1 is not None:
2223
+ self._rep_code_partners[lq2] = p1
2013
2224
  return
2014
2225
 
2015
2226
  # Boundary repetition code: redirect entirely to the 3-CNOT
@@ -2017,12 +2228,12 @@ class QrackAceBackend:
2017
2228
  # side is multi-replica (repetition-code eligible), rather than
2018
2229
  # extending each of the specialized fast/partial-match paths
2019
2230
  # below individually with new, separately-unverified encode/
2020
- # decode logic. self.cx() already handles the on-demand encode/
2021
- # couple/decode/correct cycle correctly -- verified directly,
2022
- # including real bugs found and fixed there -- so this reuses
2023
- # that rather than re-deriving equivalent logic three more times
2024
- # under the same risk of another subtle mismatch. Costs the
2025
- # fast-path optimizations below for this specific case; given
2231
+ # decode logic. self.cx() already handles PERMANENT partners
2232
+ # correctly (transversal sync, coupling extension, syndrome
2233
+ # check all live there) -- so this reuses that rather than
2234
+ # re-deriving equivalent logic three more times under the same
2235
+ # risk of another subtle mismatch. Costs the fast-path
2236
+ # optimizations below for this specific case; given
2026
2237
  # the fast/exact-match path additionally requires lq1 and lq2 to
2027
2238
  # be TWO boundary qubits with identically-matching patch
2028
2239
  # structure -- rare on its own -- the case being deferred is
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu-complex128
3
- Version: 2.19.2
3
+ Version: 2.20.0
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.19.2"
10
+ VERSION = "2.20.0"
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')