pyqrack-cpu-complex128 2.19.3__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.3 → pyqrack_cpu_complex128-2.20.0}/Makefile +1 -1
  2. {pyqrack_cpu_complex128-2.19.3/pyqrack_cpu_complex128.egg-info → pyqrack_cpu_complex128-2.20.0}/PKG-INFO +1 -1
  3. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_ace_backend.py +441 -248
  4. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0/pyqrack_cpu_complex128.egg-info}/PKG-INFO +1 -1
  5. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/setup.py +1 -1
  6. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/LICENSE +0 -0
  7. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/MANIFEST.in +0 -0
  8. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/README.md +0 -0
  9. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyproject.toml +0 -0
  10. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/__init__.py +0 -0
  11. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/neuron_activation_fn.py +0 -0
  12. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/pauli.py +0 -0
  13. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_circuit.py +0 -0
  14. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  15. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_neuron.py +0 -0
  16. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  17. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_simulator.py +0 -0
  18. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_stabilizer.py +0 -0
  19. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_system/__init__.py +0 -0
  20. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/qrack_system/qrack_system.py +0 -0
  21. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/quimb_circuit_type.py +0 -0
  22. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/stats/__init__.py +0 -0
  23. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/stats/load_quantized_data.py +0 -0
  24. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack/stats/quantize_by_range.py +0 -0
  25. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/SOURCES.txt +0 -0
  26. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/dependency_links.txt +0 -0
  27. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/not-zip-safe +0 -0
  28. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/requires.txt +0 -0
  29. {pyqrack_cpu_complex128-2.19.3 → pyqrack_cpu_complex128-2.20.0}/pyqrack_cpu_complex128.egg-info/top_level.txt +0 -0
  30. {pyqrack_cpu_complex128-2.19.3 → 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.3
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
@@ -635,30 +693,21 @@ class QrackAceBackend:
635
693
  p2 = self.sim[q2[0]].prob(q2[1]) if isinstance(q2, tuple) else q2.prob()
636
694
  target = q2
637
695
 
638
- # 0/1-BALANCE FIX: a near-tie (both readings within epsilon of each
639
- # other -- the ORDINARY case for a maximally-mixed control paired
640
- # with a freshly-H'd target, both landing at EXACTLY 0.5) carries
641
- # no informative signal about which basis state to commit to. The
642
- # previous logic computed a "prob_max" from this near-tie and
643
- # compared it against a ">= 0.5 - epsilon" threshold -- but
644
- # prob_max, drawn from two values both AT 0.5, ALWAYS satisfies
645
- # that threshold, so the Z gate fired deterministically, every
646
- # single time, regardless of the random draw upstream. Confirmed
647
- # empirically: 20/20 trials landed the shadow replica at exactly
648
- # prob=1.0, never 0.5 or anything reflecting genuine 50/50
649
- # uncertainty. A real CX from a mixed control onto a fresh target
650
- # leaves the target's own marginal at a genuine 0.5, not a
651
- # deterministic bias toward |1>.
652
- #
653
- # Fix: when the two readings are genuinely tied, the DECISION of
654
- # whether to apply Z must itself be the random draw (not merely
655
- # which value gets compared against the threshold, which never
656
- # changed the deterministic outcome). When the readings are
657
- # NOT tied, the existing decisive-threshold logic is unambiguous
658
- # and unchanged.
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.
659
709
  if (abs(p1 - 0.5) <= self._epsilon) and (abs(p2 - 0.5) <= self._epsilon):
660
710
  apply = random.random() < 0.5
661
- target = q1 if (random.random() < 0.5) else q2
662
711
  else:
663
712
  # Symmetric-in-control-vs-target CZ shadow, by design: pick
664
713
  # whichever of p1/p2 is more DECISIVE (further from 0.5, i.e.
@@ -670,6 +719,13 @@ class QrackAceBackend:
670
719
  # control definitively |0> paired with a freshly-H'd target
671
720
  # sitting at p=0.5, where the old logic fired regardless of
672
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.
673
729
  d1 = abs(p1 - 0.5)
674
730
  d2 = abs(p2 - 0.5)
675
731
  if d1 >= d2:
@@ -685,29 +741,55 @@ class QrackAceBackend:
685
741
  else:
686
742
  target.z()
687
743
 
688
- def _qec_x(self, c):
689
- if isinstance(c, tuple):
690
- self.sim[c[0]].x(c[1])
691
- else:
692
- c.x()
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()
693
772
 
694
- def _qec_h(self, t):
695
- if isinstance(t, tuple):
696
- self.sim[t[0]].h(t[1])
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
697
779
  else:
698
- t.h()
780
+ apply = p > 0.5
699
781
 
700
- def _qec_s(self, t):
701
- if isinstance(t, tuple):
702
- self.sim[t[0]].s(t[1])
703
- else:
704
- t.s()
782
+ if apply:
783
+ if isinstance(t, tuple):
784
+ getattr(self.sim[t[0]], gate_name)(t[1])
785
+ else:
786
+ getattr(t, gate_name)()
705
787
 
706
- def _qec_adjs(self, t):
707
- if isinstance(t, tuple):
708
- self.sim[t[0]].adjs(t[1])
788
+ def _qec_x(self, c):
789
+ if isinstance(c, tuple):
790
+ self.sim[c[0]].x(c[1])
709
791
  else:
710
- t.adjs()
792
+ c.x()
711
793
 
712
794
  def _detect_swap(self, sim_id, idx1, idx2):
713
795
  self.sim[sim_id].swap(idx1, idx2)
@@ -721,31 +803,13 @@ class QrackAceBackend:
721
803
  self._anti_shadow_wrap(c, t, self._cz_shadow)
722
804
 
723
805
  def _cx_shadow(self, c, t):
724
- p = self.sim[c[0]].prob(c[1]) if isinstance(c, tuple) else c.prob()
725
- if abs(p - 0.5) <= self._epsilon:
726
- apply = random.random() < 0.5
727
- else:
728
- apply = p > 0.5
729
- if apply:
730
- if isinstance(t, tuple):
731
- self.sim[t[0]].x(t[1])
732
- else:
733
- t.x()
806
+ self._shadow_control_apply(c, t, "x")
734
807
 
735
808
  def _anti_cx_shadow(self, c, t):
736
809
  self._anti_shadow_wrap(c, t, self._cx_shadow)
737
810
 
738
811
  def _cy_shadow(self, c, t):
739
- p = self.sim[c[0]].prob(c[1]) if isinstance(c, tuple) else c.prob()
740
- if abs(p - 0.5) <= self._epsilon:
741
- apply = random.random() < 0.5
742
- else:
743
- apply = p > 0.5
744
- if apply:
745
- if isinstance(t, tuple):
746
- self.sim[t[0]].y(t[1])
747
- else:
748
- t.y()
812
+ self._shadow_control_apply(c, t, "y")
749
813
 
750
814
  def _anti_cy_shadow(self, c, t):
751
815
  self._anti_shadow_wrap(c, t, self._cy_shadow)
@@ -1109,6 +1173,26 @@ class QrackAceBackend:
1109
1173
  math.atan(math.tan(i) * b) - i,
1110
1174
  )
1111
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
+
1112
1196
  def u(self, lq, th, ph, lm):
1113
1197
  hq = self._unpack(lq)
1114
1198
  if len(hq) < 2:
@@ -1129,6 +1213,8 @@ class QrackAceBackend:
1129
1213
  if lhv is not None:
1130
1214
  lhv.u(th, ph, lm)
1131
1215
 
1216
+ self._rep_code_apply_gate(lq, "u", th, ph, lm)
1217
+
1132
1218
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
1133
1219
 
1134
1220
  def _revert_shadow_commitment(self, lq1):
@@ -1222,6 +1308,11 @@ class QrackAceBackend:
1222
1308
  elif p == Pauli.PauliZ:
1223
1309
  lhv.rz(th)
1224
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
+
1225
1316
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
1226
1317
 
1227
1318
  def h(self, lq):
@@ -1244,6 +1335,8 @@ class QrackAceBackend:
1244
1335
  if lhv is not None:
1245
1336
  lhv.h()
1246
1337
 
1338
+ self._rep_code_apply_gate(lq, "h")
1339
+
1247
1340
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
1248
1341
 
1249
1342
  def s(self, lq):
@@ -1266,6 +1359,8 @@ class QrackAceBackend:
1266
1359
  if lhv is not None:
1267
1360
  lhv.s()
1268
1361
 
1362
+ self._rep_code_apply_gate(lq, "s")
1363
+
1269
1364
  def adjs(self, lq):
1270
1365
  hq = self._unpack(lq)
1271
1366
  if len(hq) < 2:
@@ -1286,6 +1381,8 @@ class QrackAceBackend:
1286
1381
  if lhv is not None:
1287
1382
  lhv.adjs()
1288
1383
 
1384
+ self._rep_code_apply_gate(lq, "adjs")
1385
+
1289
1386
  def sx(self, lq):
1290
1387
  hq = self._unpack(lq)
1291
1388
  if len(hq) < 2:
@@ -1306,6 +1403,8 @@ class QrackAceBackend:
1306
1403
  if lhv is not None:
1307
1404
  lhv.sx()
1308
1405
 
1406
+ self._rep_code_apply_gate(lq, "sx")
1407
+
1309
1408
  def adjsx(self, lq):
1310
1409
  hq = self._unpack(lq)
1311
1410
  if len(hq) < 2:
@@ -1326,129 +1425,147 @@ class QrackAceBackend:
1326
1425
  if lhv is not None:
1327
1426
  lhv.adjsx()
1328
1427
 
1329
- def _rep_code_patch_triples(self, lq):
1330
- """(patch_replica, ancilla1_lq, ancilla2_lq) triples for lq's
1331
- non-crossbar patches -- one per patch this boundary qubit
1332
- touches. Recomputed fresh on every call (this is the on-demand
1333
- design: nothing persists between coupling events), rather than
1334
- stored once at construction the way the earlier, always-on
1335
- version did. Empty if lq is bulk (single-replica) or the
1336
- feature is off -- both cases where there's nothing to protect
1337
- 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.
1338
1436
  """
1339
1437
  if not self.is_boundary_repetition_code:
1340
1438
  return []
1341
- hq = self._qubits[lq]
1342
- if len(hq) < 2:
1439
+ partners = self._rep_code_partners.get(lq)
1440
+ if not partners:
1343
1441
  return []
1344
- triples = []
1442
+ hq = self._qubits[lq]
1443
+ pairs = []
1444
+ p_i = 0
1345
1445
  for replica in hq:
1346
1446
  if self._boundary_sim_id is not None and replica[0] == self._boundary_sim_id:
1347
1447
  continue
1348
- sim_id = replica[0]
1349
- triples.append((replica, self._rep_code_ancilla1_lq[sim_id], self._rep_code_ancilla2_lq[sim_id]))
1350
- return triples
1351
-
1352
- def _rep_code_encode(self, lq):
1353
- """Step 1: CNOT lq's replica-in-each-patch onto that patch's
1354
- dedicated, reusable ancilla1 -- a fresh, TEMPORARY repetition-
1355
- code copy, good for exactly the duration of one coupling event.
1356
- Returns the triples for the caller to pass to
1357
- _rep_code_extend_coupling and _rep_code_decode_and_correct, or
1358
- 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.
1359
1459
  """
1360
- triples = self._rep_code_patch_triples(lq)
1361
- for (sim_id, b_idx), anc1_lq, _ in triples:
1362
- anc1_idx = self._qubits[anc1_lq][0][1]
1363
- self.sim[sim_id].mcx([b_idx], anc1_idx)
1364
- return triples
1365
-
1366
- def _rep_code_extend_coupling(self, pauli, anti, qb1, hq1, lq1_lr, lq1, triples):
1367
- """Step 2: extend the SAME coupling (same control) to each
1368
- freshly-encoded ancilla1 -- reuses _apply_coupling directly,
1369
- the same real/shadow dispatch every other target already gets.
1370
- Only meaningful for a TARGET's triples: a CONTROL's own value
1371
- is never touched by gate_fn/shadow_fn in the first place (the
1372
- established, control-invariance fact this whole file already
1373
- relies on), so there's nothing for a control's ancilla1 to need
1374
- extended to -- callers only pass a target's triples here.
1375
- """
1376
- for _, anc1_lq, _ in triples:
1377
- hq_a = self._qubits[anc1_lq]
1378
- qb_a, _ = QrackAceBackend._get_qb_lhv_indices(hq_a)
1379
- self._apply_coupling(pauli, anti, qb1, hq1, qb_a, hq_a, lq1_lr, lq1, anc1_lq)
1380
-
1381
- def _rep_code_decode_and_correct(self, lq, triples, phase=False):
1382
- """Steps 3-5: bit-flip syndrome-check-and-correct (using
1383
- ancilla2 per patch), THEN the final decode CNOT (lq's replica
1384
- -> ancilla1 again), which -- once any disagreement has already
1385
- been corrected -- returns ancilla1 exactly to |0>, ready for
1386
- the next boundary qubit's coupling event to reuse. An explicit
1387
- measure-and-reset backstops the decode CNOT for the rare
1388
- (corner-only) multi-disagreement case left uncorrected below,
1389
- 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.
1390
1475
 
1391
1476
  Same "try to force error-free, fall back to an unforced Born-
1392
1477
  rule read, then it's obvious by elimination" decoding pattern
1393
- used elsewhere in this file. With N syndrome bits (2 for an
1394
- edge qubit's 3-qubit code, 4 for a corner's 5-qubit code): zero
1395
- disagreeing patches means no error; exactly one unambiguously
1396
- identifies that patch's ancilla1 as the outlier (every other
1397
- patch's ancilla1, by construction, still agrees with lq); a
1398
- strict majority (including unanimity) unambiguously identifies
1399
- lq itself as the outlier. A strict minority greater than one is
1400
- not explainable by any single-qubit error and is left
1401
- uncorrected -- guessing would risk introducing a wrong
1402
- correction where an unrelated, uncorrectable multi-qubit event
1403
- 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.
1404
1488
  """
1405
- if not triples:
1489
+ if not pairs:
1406
1490
  return
1407
1491
 
1408
1492
  syndrome = []
1409
- for (sim_id, b_idx), anc1_lq, anc2_lq in triples:
1493
+ for (sim_id, b_idx), p_lq, syn_lq in pairs:
1410
1494
  sim = self.sim[sim_id]
1411
- anc1_idx = self._qubits[anc1_lq][0][1]
1412
- anc2_idx = self._qubits[anc2_lq][0][1]
1413
-
1414
- sim.mcx([b_idx], anc2_idx)
1415
- sim.mcx([anc1_idx], anc2_idx)
1416
-
1417
- p = sim.prob(anc2_idx)
1418
- 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)
1419
1516
  if bit:
1420
- sim.x(anc2_idx) # reset ancilla2 to |0> for reuse
1517
+ sim.x(syn_idx) # reset the shared syndrome ancilla for reuse
1421
1518
 
1422
- syndrome.append((bit, sim_id, anc1_idx))
1519
+ syndrome.append((bit, sim_id, p_idx))
1423
1520
 
1424
1521
  n = len(syndrome)
1425
1522
  disagree = sum(1 for bit, _, _ in syndrome if bit)
1426
- if disagree > 0:
1427
- if disagree > n - disagree:
1428
- # 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:
1429
1535
  if phase:
1430
1536
  self.z(lq)
1431
1537
  else:
1432
1538
  self.x(lq)
1433
- # Necessary either way, even if all flipped
1434
- 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:
1435
1544
  if bit:
1436
1545
  if phase:
1437
- self.sim[sim_id].z(anc1_idx)
1546
+ self.sim[sim_id].z(p_idx)
1438
1547
  else:
1439
- self.sim[sim_id].x(anc1_idx)
1440
-
1441
- for (sim_id, b_idx), anc1_lq, _ in triples:
1442
- anc1_idx = self._qubits[anc1_lq][0][1]
1443
- sim = self.sim[sim_id]
1444
- sim.mcx([b_idx], anc1_idx)
1445
- p = sim.prob(anc1_idx)
1446
- if self._ps_epsilon >= (1.0 - p):
1447
- b = sim.m(anc1_idx)
1448
- else:
1449
- b = sim.force_m(anc1_idx, False)
1450
- if b:
1451
- 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)
1452
1569
 
1453
1570
  def x(self, lq):
1454
1571
  hq = self._unpack(lq)
@@ -1470,6 +1587,8 @@ class QrackAceBackend:
1470
1587
  if lhv is not None:
1471
1588
  lhv.x()
1472
1589
 
1590
+ self._rep_code_apply_gate(lq, "x")
1591
+
1473
1592
  def y(self, lq):
1474
1593
  hq = self._unpack(lq)
1475
1594
  if len(hq) < 2:
@@ -1490,6 +1609,8 @@ class QrackAceBackend:
1490
1609
  if lhv is not None:
1491
1610
  lhv.y()
1492
1611
 
1612
+ self._rep_code_apply_gate(lq, "y")
1613
+
1493
1614
  def z(self, lq):
1494
1615
  hq = self._unpack(lq)
1495
1616
  if len(hq) < 2:
@@ -1510,6 +1631,8 @@ class QrackAceBackend:
1510
1631
  if lhv is not None:
1511
1632
  lhv.z()
1512
1633
 
1634
+ self._rep_code_apply_gate(lq, "z")
1635
+
1513
1636
  def t(self, lq):
1514
1637
  hq = self._unpack(lq)
1515
1638
  if len(hq) < 2:
@@ -1530,6 +1653,8 @@ class QrackAceBackend:
1530
1653
  if lhv is not None:
1531
1654
  lhv.t()
1532
1655
 
1656
+ self._rep_code_apply_gate(lq, "t")
1657
+
1533
1658
  def adjt(self, lq):
1534
1659
  hq = self._unpack(lq)
1535
1660
  if len(hq) < 2:
@@ -1550,6 +1675,8 @@ class QrackAceBackend:
1550
1675
  if lhv is not None:
1551
1676
  lhv.adjt()
1552
1677
 
1678
+ self._rep_code_apply_gate(lq, "adjt")
1679
+
1553
1680
  def _get_gate(self, pauli, anti, sim_id):
1554
1681
  gate = None
1555
1682
  shadow = None
@@ -1790,40 +1917,34 @@ class QrackAceBackend:
1790
1917
  self._correct(lq1)
1791
1918
  self._correct(lq2)
1792
1919
 
1793
- # Boundary repetition code, on-demand: encode/couple/decode/
1794
- # correct wraps ONLY the actual coupler call immediately below,
1795
- # tightly -- not the transversal single-qubit gates (already
1796
- # exact, per-replica, nothing to protect), and not the outer
1797
- # cy()/cz() wrapper when this _cpauli call is their internal,
1798
- # H-sandwiched cx() (see below for why that's actually fine now,
1799
- # unlike the earlier design). Guarded by not
1800
- # self._in_gadget_capture the same way the _correct() calls
1801
- # above are: a nested capture's own target is always a hidden,
1802
- # single-replica ancilla (never boundary-eligible, so this would
1803
- # no-op anyway), but the CONTROL side could still be a protected
1804
- # boundary qubit, and re-running its own encode/check/decode
1805
- # cycle on every nested capture would be wasteful and redundant
1806
- # 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.
1807
1930
  #
1808
- # lq1 (control) gets a SIMPLER cycle than lq2 (target): the
1809
- # coupler never gates the control at all (the same control-
1810
- # invariance fact this whole file already relies on), so there's
1811
- # no coupling effect to extend to its ancilla1 -- just verify/
1812
- # correct lq1's OWN reliability once, right before the coupler
1813
- # is about to read it, so a stale value doesn't feed a wrong
1814
- # shadow decision.
1815
- if self.is_boundary_repetition_code and not self._in_gadget_capture:
1816
- t1 = self._rep_code_encode(lq1)
1817
- self._rep_code_decode_and_correct(lq1, t1)
1818
-
1819
- # lq2 (target) encode happens BEFORE the coupler, so the SAME
1820
- # coupling can be extended to ancilla1 right after -- capturing
1821
- # a "before" snapshot the coupler's own effect then lands on
1822
- # too, the same real/shadow dispatch every other target gets.
1823
- t2 = []
1824
- if self.is_boundary_repetition_code and not self._in_gadget_capture:
1825
- t2 = self._rep_code_encode(lq2)
1826
- 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:
1827
1948
  # Only defined for "patch-local" connectivity, the only
1828
1949
  # officially supported connectivity model here (get_
1829
1950
  # logical_coupling_map() only ever returns pairs that
@@ -1831,7 +1952,7 @@ class QrackAceBackend:
1831
1952
  # share NO simulator at all) only exists as a fallback
1832
1953
  # for users who ignore that map, and this repetition-
1833
1954
  # code layer doesn't try to support it: raise plainly
1834
- # instead of silently coupling ancilla1 through an
1955
+ # instead of silently coupling partners through an
1835
1956
  # undefined/unsupported path.
1836
1957
  sims1 = {b[0] for b in hq1}
1837
1958
  sims2 = {b[0] for b in hq2}
@@ -1849,8 +1970,8 @@ class QrackAceBackend:
1849
1970
  # qubits, which now live as real qubits in the shared boundary sim.
1850
1971
  self._apply_coupling(pauli, anti, qb1, hq1, qb2, hq2, lq1_lr, lq1, lq2)
1851
1972
 
1852
- if t2:
1853
- 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)
1854
1975
 
1855
1976
  if lq2 in self._lhv:
1856
1977
  ctrl_prob = self.sim[hq1[0][0]].prob(hq1[0][1])
@@ -1859,21 +1980,16 @@ class QrackAceBackend:
1859
1980
  if self._in_gadget_capture:
1860
1981
  return
1861
1982
 
1862
- # Repetition-code decode-and-correct for lq2 happens HERE,
1983
+ # Repetition-code syndrome-check for lq2 happens HERE,
1863
1984
  # immediately after the coupler+extension, in the SAME frame
1864
1985
  # they just ran in -- deliberately BEFORE _correct()'s own,
1865
1986
  # separate soft reconciliation below, which can itself rotate
1866
- # lq2 (the phase=True call) and would otherwise pull it out of
1867
- # sync with an ancilla1 that never gets that same rotation. This
1868
- # is also exactly why cy()/cz()'s internal H-sandwich is safe
1869
- # now, unlike the earlier, always-on design: encode, couple,
1870
- # and this decode-and-correct are all contained within this one
1871
- # _cpauli call, whatever frame it happens to be running in --
1872
- # never spanning an external rotation the way permanently-
1873
- # synced partners, checked from a separate top-level call, used
1874
- # to.
1875
- if self.is_boundary_repetition_code:
1876
- 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)
1877
1993
 
1878
1994
  # No post-coupling correction on lq1 (the control): _cz_shadow only
1879
1995
  # ever calls .z() on the target, never gates the control -- only
@@ -1894,16 +2010,74 @@ class QrackAceBackend:
1894
2010
  def cy(self, lq1, lq2):
1895
2011
  if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1896
2012
  self.adjs(lq2)
1897
- 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
1898
2018
  self.s(lq2)
2019
+ self._rep_code_check_pair(lq1, lq2)
1899
2020
  else:
1900
2021
  self._cpauli(lq1, lq2, False, Pauli.PauliY)
1901
2022
 
1902
2023
  def cz(self, lq1, lq2):
1903
2024
  if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1904
- self.h(lq2)
1905
- self.cx(lq1, lq2)
1906
- 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)
1907
2081
  else:
1908
2082
  self._cpauli(lq1, lq2, False, Pauli.PauliZ)
1909
2083
 
@@ -1913,16 +2087,26 @@ class QrackAceBackend:
1913
2087
  def acy(self, lq1, lq2):
1914
2088
  if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1915
2089
  self.adjs(lq2)
1916
- 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
1917
2095
  self.s(lq2)
2096
+ self._rep_code_check_pair(lq1, lq2)
1918
2097
  else:
1919
2098
  self._cpauli(lq1, lq2, True, Pauli.PauliY)
1920
2099
 
1921
2100
  def acz(self, lq1, lq2):
1922
2101
  if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1923
2102
  self.h(lq2)
1924
- 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
1925
2108
  self.h(lq2)
2109
+ self._rep_code_check_pair(lq1, lq2)
1926
2110
  else:
1927
2111
  self._cpauli(lq1, lq2, True, Pauli.PauliZ)
1928
2112
 
@@ -2021,13 +2205,22 @@ class QrackAceBackend:
2021
2205
  if lhv1 is not None:
2022
2206
  self._lhv[lq2] = lhv1
2023
2207
 
2024
- # Boundary repetition code needs no bookkeeping here, unlike
2025
- # self._qubits/self._lhv just above: it's on-demand now, not
2026
- # a permanent, per-logical-index dict of pre-allocated
2027
- # partners -- eligibility is just len(self._qubits[lq]) > 1,
2028
- # computed fresh on every coupling event, so the reference
2029
- # swap of self._qubits above already, automatically carries
2030
- # 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
2031
2224
  return
2032
2225
 
2033
2226
  # Boundary repetition code: redirect entirely to the 3-CNOT
@@ -2035,12 +2228,12 @@ class QrackAceBackend:
2035
2228
  # side is multi-replica (repetition-code eligible), rather than
2036
2229
  # extending each of the specialized fast/partial-match paths
2037
2230
  # below individually with new, separately-unverified encode/
2038
- # decode logic. self.cx() already handles the on-demand encode/
2039
- # couple/decode/correct cycle correctly -- verified directly,
2040
- # including real bugs found and fixed there -- so this reuses
2041
- # that rather than re-deriving equivalent logic three more times
2042
- # under the same risk of another subtle mismatch. Costs the
2043
- # 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
2044
2237
  # the fast/exact-match path additionally requires lq1 and lq2 to
2045
2238
  # be TWO boundary qubits with identically-matching patch
2046
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.3
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.3"
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')