pyqrack-complex128 2.18.2__tar.gz → 2.18.3__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 (33) hide show
  1. {pyqrack_complex128-2.18.2/pyqrack_complex128.egg-info → pyqrack_complex128-2.18.3}/PKG-INFO +1 -1
  2. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_ace_backend.py +229 -147
  3. pyqrack_complex128-2.18.3/pyqrack/qrack_system/qrack_cl_precompile/qrack_cl_precompile +0 -0
  4. pyqrack_complex128-2.18.3/pyqrack/qrack_system/qrack_lib/libqrack_pinvoke.so +0 -0
  5. pyqrack_complex128-2.18.3/pyqrack/qrack_system/qrack_lib/libqrack_pinvoke.so.10.13.0 +0 -0
  6. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3/pyqrack_complex128.egg-info}/PKG-INFO +1 -1
  7. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack_complex128.egg-info/SOURCES.txt +3 -0
  8. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/setup.py +1 -1
  9. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/LICENSE +0 -0
  10. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/MANIFEST.in +0 -0
  11. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/Makefile +0 -0
  12. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/README.md +0 -0
  13. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyproject.toml +0 -0
  14. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/__init__.py +0 -0
  15. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/neuron_activation_fn.py +0 -0
  16. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/pauli.py +0 -0
  17. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_circuit.py +0 -0
  18. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  19. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_neuron.py +0 -0
  20. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  21. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_simulator.py +0 -0
  22. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_stabilizer.py +0 -0
  23. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_system/__init__.py +0 -0
  24. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/qrack_system/qrack_system.py +0 -0
  25. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/quimb_circuit_type.py +0 -0
  26. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/stats/__init__.py +0 -0
  27. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/stats/load_quantized_data.py +0 -0
  28. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack/stats/quantize_by_range.py +0 -0
  29. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack_complex128.egg-info/dependency_links.txt +0 -0
  30. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack_complex128.egg-info/not-zip-safe +0 -0
  31. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack_complex128.egg-info/requires.txt +0 -0
  32. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/pyqrack_complex128.egg-info/top_level.txt +0 -0
  33. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-complex128
3
- Version: 2.18.2
3
+ Version: 2.18.3
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -274,11 +274,11 @@ class QrackAceBackend:
274
274
  if "QRACK_FPPOW" in os.environ:
275
275
  fppow = int(os.environ.get("QRACK_FPPOW"))
276
276
  if fppow < 5:
277
- self._epsilon = 2**-9
277
+ self._epsilon = 2**-8
278
278
  elif fppow > 5:
279
- self._epsilon = 2**-51
279
+ self._epsilon = 2**-50
280
280
  else:
281
- self._epsilon = 2**-22
281
+ self._epsilon = 2**-21
282
282
 
283
283
  self._coupling_map = None
284
284
 
@@ -406,18 +406,57 @@ class QrackAceBackend:
406
406
  sim_counts.append(boundary_count)
407
407
 
408
408
  # Error-detection gadget (IBM-style detect-and-post-select, not
409
- # correction): one shared ancilla per simulator -- every patch AND
409
+ # correction): two shared ancillae per simulator -- every patch AND
410
410
  # the boundary crossbar sim alike, "a single ancilla per patch,
411
411
  # overall" -- reused across every real (same-simulator) coupling
412
412
  # gate that touches it, never allocated per-gate or per-qubit.
413
413
  # See _apply_coupling for the actual gadget; this block only does
414
414
  # the qubit-index bookkeeping, in the same style as every other
415
415
  # per-simulator allocation above.
416
- self._detect_ancilla = []
416
+ self._detect_ancilla1 = []
417
+ self._detect_ancilla2 = []
417
418
  if self.is_error_detection:
418
419
  for i in range(len(sim_counts)):
419
- self._detect_ancilla.append(sim_counts[i])
420
+ self._detect_ancilla1.append(sim_counts[i])
420
421
  sim_counts[i] += 1
422
+ self._detect_ancilla2.append(sim_counts[i])
423
+ sim_counts[i] += 1
424
+
425
+ # Logical-qubit wrapper around each patch's detection ancilla,
426
+ # so it's addressable via the ORDINARY cx()/_cpauli() machinery
427
+ # (self.cx(lq1, anc)), not just raw (sim, idx) tuples. Appended
428
+ # PAST the user-facing qubit range (num_qubits() is computed
429
+ # independently from the grid dimensions, not from len(
430
+ # self._qubits), so these extra entries are never visible or
431
+ # reachable through the normal API) -- a simple, single-replica
432
+ # (bulk-style) logical qubit per simulator, one entry in
433
+ # self._qubits pointing at the same physical qubit _detect_
434
+ # ancilla already allocated there. See _apply_coupling for why
435
+ # this needs to be a genuine logical qubit and not just a raw
436
+ # physical capture: capturing lq1's LOGICAL value via a real,
437
+ # nested cx(lq1, anc) call goes through the full _correct()/
438
+ # shadow-coupling machinery robustly, the same way any other
439
+ # logical qubit's value would be determined -- watching one
440
+ # specific physical replica directly (an earlier version of
441
+ # this) isn't a valid invariant, since _correct() can
442
+ # legitimately need to change any single replica's own value.
443
+ self._detect_ancilla1_lq = []
444
+ self._detect_ancilla2_lq = []
445
+ if self.is_error_detection:
446
+ for sim_id, phys_idx in enumerate(self._detect_ancilla1):
447
+ lq_idx = len(self._qubits)
448
+ self._qubits.append([(sim_id, phys_idx)])
449
+ self._detect_ancilla1_lq.append(lq_idx)
450
+ for sim_id, phys_idx in enumerate(self._detect_ancilla2):
451
+ lq_idx = len(self._qubits)
452
+ self._qubits.append([(sim_id, phys_idx)])
453
+ self._detect_ancilla2_lq.append(lq_idx)
454
+ # Reentrancy guard for the gadget's own nested cx(lq1, anc)
455
+ # capture calls (see _apply_coupling): those calls go through
456
+ # the ordinary _cpauli -> _apply_coupling path themselves, which
457
+ # would otherwise try to wrap ITSELF in another capture,
458
+ # recursing without end.
459
+ self._in_gadget_capture = False
421
460
 
422
461
  if "QRACK_QUNIT_SEPARABILITY_THRESHOLD" in os.environ:
423
462
  self._sdrp = min(1, float(os.environ["QRACK_QUNIT_SEPARABILITY_THRESHOLD"]))
@@ -632,51 +671,12 @@ class QrackAceBackend:
632
671
  t.adjs()
633
672
 
634
673
  def _detect_swap(self, sim_id, idx1, idx2):
635
- """Same detect-and-post-select gadget as _apply_coupling's real
636
- gate branch, generalized to SWAP's own invariant instead of
637
- CNOT's. SWAP exchanges two qubits' values, but their XOR
638
- (parity) is exactly conserved in the noiseless case:
639
- idx1_before ^ idx2_before == idx1_after ^ idx2_after, regardless
640
- of either qubit's individual value -- captured the same way,
641
- just with two capture-CNOTs (one per qubit) into the shared
642
- per-simulator ancilla instead of one.
643
- """
644
- sim = self.sim[sim_id]
645
- if not self.is_error_detection:
646
- sim.swap(idx1, idx2)
647
- return
648
- anc = self._detect_ancilla[sim_id]
649
- sim.mcx([idx1], anc)
650
- sim.mcx([idx2], anc)
651
- sim.swap(idx1, idx2)
652
- sim.mcx([idx1], anc)
653
- sim.mcx([idx2], anc)
654
- sim.force_m(anc, False)
674
+ self.sim[sim_id].swap(idx1, idx2)
655
675
 
656
676
  def _anti_shadow_wrap(self, c, t, middle_fn):
657
- """Shared gadget for _anti_c{x,y,z}_shadow: all three bracket
658
- their genuinely cross-sim shadow call with two _qec_x(c) calls,
659
- and nothing else ever touches c in between -- the shadow call
660
- underneath only ever reads c via .prob(), never gates it (same
661
- property _apply_coupling's real branch relies on for its own
662
- control-invariance check). So X;X on c should net to identity,
663
- in the noiseless case, regardless of c's current value or basis
664
- -- exactly as protectable as the CNOT-sandwich case, just with
665
- X instead of CNOT as the bracketing gate.
666
- """
667
- if not (self.is_error_detection and isinstance(c, tuple)):
668
- self._qec_x(c)
669
- middle_fn(c, t)
670
- self._qec_x(c)
671
- return
672
- anc = self._detect_ancilla[c[0]]
673
- sim = self.sim[c[0]]
674
- sim.mcx([c[1]], anc)
675
677
  self._qec_x(c)
676
678
  middle_fn(c, t)
677
679
  self._qec_x(c)
678
- sim.mcx([c[1]], anc)
679
- sim.force_m(anc, False)
680
680
 
681
681
  def _anti_cz_shadow(self, c, t):
682
682
  self._anti_shadow_wrap(c, t, self._cz_shadow)
@@ -1444,6 +1444,56 @@ class QrackAceBackend:
1444
1444
  # bounded window, no reactive invalidation
1445
1445
  # needed for these -- see
1446
1446
  # _resolve_witnessed_shadow.
1447
+
1448
+ # Logical-level error-detection gadget: captures lq1's LOGICAL
1449
+ # value (and lq2's too, for CZ specifically -- see below) into a
1450
+ # dedicated hidden ancilla via a genuine, nested cx() call --
1451
+ # NOT a raw capture of one specific physical replica. This
1452
+ # matters: gate_fn/shadow_fn below never gate the control (only
1453
+ # ever read it via .prob()), so lq1's LOGICAL value is
1454
+ # structurally guaranteed not to change across this loop --
1455
+ # that invariant holds regardless of what _correct() may have
1456
+ # legitimately done to any INDIVIDUAL replica beforehand,
1457
+ # because the nested cx(lq1, anc) call determines lq1's value
1458
+ # the same robust way any other logical coupling would, not by
1459
+ # trusting one physical replica directly (an earlier version of
1460
+ # this tried exactly that and crashed: _correct() can
1461
+ # legitimately need to change hq1[0]'s own value, so "hq1[0]
1462
+ # never changes" isn't actually a valid invariant, even though
1463
+ # "lq1's logical value never changes here" is).
1464
+ #
1465
+ # The genuine syndrome signal comes from the capture itself: the
1466
+ # nested cx() goes through the same _correct()/shadow-coupling
1467
+ # machinery as any other logical operation, which can introduce
1468
+ # some imprecision between the "before" and "after" capture even
1469
+ # though lq1's true value didn't change -- that imprecision is
1470
+ # exactly this architecture's own intrinsic (not noise=p)
1471
+ # approximation error, which is what's worth catching.
1472
+ #
1473
+ # CZ also captures lq2 into the SAME ancilla: unlike CX/CY,
1474
+ # where the target's value legitimately, intentionally changes
1475
+ # (that's the entangling operation, not an error), CZ never
1476
+ # changes either participant's Z-population on any input, so
1477
+ # lq2's logical value is an equally valid invariant here, for
1478
+ # this gate family only -- same reasoning as the earlier
1479
+ # single-replica CZ dual-check, just applied at the logical
1480
+ # level now.
1481
+ anc1, anc2 = None, None
1482
+ if self.is_error_detection and not self._in_gadget_capture:
1483
+ anc1 = self._detect_ancilla1_lq[hq1[0][0]]
1484
+ self._in_gadget_capture = True
1485
+ # Control bit-flip
1486
+ self.cx(lq1, anc1)
1487
+ if len(hq1) > 1:
1488
+ anc2 = self._detect_ancilla2_lq[hq1[0][0]]
1489
+ # XOR on target
1490
+ self.cx(lq2, anc2)
1491
+ if anti:
1492
+ self.acx(lq1, anc2)
1493
+ else:
1494
+ self.cx(lq1, anc2)
1495
+ self._in_gadget_capture = False
1496
+
1447
1497
  for q1 in qb1:
1448
1498
  b1 = hq1[q1]
1449
1499
  gate_fn, shadow_fn = self._get_gate(pauli, anti, b1[0])
@@ -1455,88 +1505,56 @@ class QrackAceBackend:
1455
1505
  for q2 in qb2:
1456
1506
  b2 = hq2[q2]
1457
1507
  if b1[0] == b2[0]:
1458
- if self.is_error_detection:
1459
- # Single-patch, not multi-patch: force_m's
1460
- # post-selection isn't local to the one
1461
- # simulator it's physically called on -- the
1462
- # projection it performs reflects the "lab
1463
- # frame" ground truth for whatever it's checking,
1464
- # not an independent per-patch fact. Since every
1465
- # replica of a boundary qubit is meant to
1466
- # represent the SAME logical value, checking that
1467
- # invariant via any one patch that has a real
1468
- # gate touching it already captures the full
1469
- # syndrome -- separately re-checking lq1's other
1470
- # replicas in their own patches doesn't add new
1471
- # independent information, it just adds their
1472
- # own extra gate exposure for no additional
1473
- # benefit. So: XOR b1's value into ITS OWN
1474
- # patch's ancilla immediately before the gate,
1475
- # XOR it in again immediately after, force_m that
1476
- # one ancilla -- b1 only, not every replica of
1477
- # lq1.
1478
- #
1479
- # Also worth being explicit about: what this
1480
- # catches is NOT the same thing as the `noise=`
1481
- # parameter's injected depolarizing/Pauli channel.
1482
- # It's this architecture's own INTRINSIC error --
1483
- # the approximation inherent in the elided
1484
- # shadow-coupling / replica-consistency scheme,
1485
- # present even at noise=0. Testing this gadget
1486
- # against `noise=p` instead measures something
1487
- # different (and actively penalizes it, since the
1488
- # sandwiching CNOTs are then subject to that same
1489
- # injected noise too) -- see conversation record.
1490
- #
1491
- # Scope: still only the mcx/mcy/mcz path
1492
- # (_cpauli / _apply_coupling); swap/iswap/etc.
1493
- # get their own dedicated gadget (_detect_swap),
1494
- # and the anti-controlled shadow variants get
1495
- # theirs (_anti_shadow_wrap) -- see those.
1496
- #
1497
- # CZ specifically also checks b2, not just b1:
1498
- # unlike CX/CY, where the target's Z-population
1499
- # legitimately, intentionally changes (that IS
1500
- # the entangling operation -- not an error), CZ
1501
- # is diagonal in the computational basis and
1502
- # NEVER changes either qubit's Z-population, only
1503
- # a relative phase. So b2 is an equally valid
1504
- # invariance check here, for this gate family
1505
- # only. Combined as one XOR parity (b1^b2), same
1506
- # pattern as the SWAP gadget: catches either
1507
- # participant's first-order (O(p)) disturbance,
1508
- # only misses the second-order (O(p^2)) case
1509
- # where both are disturbed simultaneously in a
1510
- # way that cancels in the combined parity.
1511
- # (Considered and rejected checking b1 in BOTH
1512
- # Z and X bases instead, to catch phase-type
1513
- # errors on the control too -- doesn't work: CX/CY
1514
- # genuinely, intentionally entangle control with
1515
- # target, which legitimately degrades the
1516
- # control's own X/Y-basis coherence as correct
1517
- # gate behavior, not error. A detector for that
1518
- # would flag every successful entangling gate and
1519
- # destroy the entanglement the gate was supposed
1520
- # to create.)
1521
- check_b2_too = pauli == Pauli.PauliZ
1522
- anc = self._detect_ancilla[b1[0]]
1523
- sim = self.sim[b1[0]]
1524
- sim.mcx([b1[1]], anc)
1525
- if check_b2_too:
1526
- sim.mcx([b2[1]], anc)
1527
- gate_fn([b1[1]], b2[1])
1528
- sim.mcx([b1[1]], anc)
1529
- if check_b2_too:
1530
- sim.mcx([b2[1]], anc)
1531
- sim.force_m(anc, False)
1532
- else:
1533
- gate_fn([b1[1]], b2[1])
1508
+ gate_fn([b1[1]], b2[1])
1534
1509
  elif lq1_lr or (b1[1] == b2[1]) or ((len(qb1) == 2) and (b1[1] == (b2[1] & 1))):
1535
1510
  shadow_fn(b1, b2)
1536
1511
  shadow_targets.append(b2)
1537
1512
  if witness is not None and witness != b2:
1538
1513
  witnessed_targets.append((b2, witness))
1539
1514
 
1515
+ if anc1 is not None:
1516
+ self._in_gadget_capture = True
1517
+ self.cx(lq1, anc1)
1518
+ # Check prob() before forcing: force_m(anc, False)
1519
+ # assumes the "agree" branch has nonzero probability,
1520
+ # which usually holds (that's the whole invariant this
1521
+ # gadget relies on) but isn't guaranteed -- if the
1522
+ # architecture's own approximation genuinely, confidently
1523
+ # drove lq1 (or lq2) to a different value between the
1524
+ # two captures, the "agree" branch can have exactly zero
1525
+ # probability, and forcing it crashes outright (verified
1526
+ # directly: reproducible RuntimeError under realistic
1527
+ # circuit depth). Accept that a genuine, confident
1528
+ # disagreement occurred instead of forcing the
1529
+ # impossible branch: force the TRUE condition (safe,
1530
+ # since that branch is the one that actually carries the
1531
+ # probability), reset the ancilla back to |0>, and apply
1532
+ # a corrective X to lq1 to restore the detected
1533
+ # invariant.
1534
+ anc_sim, anc_idx = self._qubits[anc1][0]
1535
+ p = self.sim[anc_sim].prob(anc_idx)
1536
+ if p > 0.5:
1537
+ self.force_m(anc1, True)
1538
+ self.x(anc1)
1539
+ self.x(lq1)
1540
+ else:
1541
+ self.force_m(anc1, False)
1542
+ self._in_gadget_capture = False
1543
+
1544
+ if anc2 is not None:
1545
+ self._in_gadget_capture = True
1546
+ # XOR check
1547
+ self.cx(lq2, anc2)
1548
+ anc_sim, anc_idx = self._qubits[anc2][0]
1549
+ p = self.sim[anc_sim].prob(anc_idx)
1550
+ if p > 0.5:
1551
+ self.force_m(anc2, True)
1552
+ self.x(anc2)
1553
+ self.x(lq2)
1554
+ else:
1555
+ self.force_m(anc2, False)
1556
+ self._in_gadget_capture = False
1557
+
1540
1558
  if lq2 is not None and witnessed_targets and self._witness_map is not None:
1541
1559
  wmap = self._witness_map.setdefault(lq2, {})
1542
1560
  for target_replica, witness_replica in witnessed_targets:
@@ -1621,61 +1639,81 @@ class QrackAceBackend:
1621
1639
  self._cpauli(lq1, lq2, False, Pauli.PauliX)
1622
1640
 
1623
1641
  def cy(self, lq1, lq2):
1624
- self._cpauli(lq1, lq2, False, Pauli.PauliY)
1642
+ if self.is_error_detection:
1643
+ self.adjs(lq2)
1644
+ self.cx(lq1, lq2)
1645
+ self.s(lq2)
1646
+ else:
1647
+ self._cpauli(lq1, lq2, False, Pauli.PauliY)
1625
1648
 
1626
1649
  def cz(self, lq1, lq2):
1627
- self._cpauli(lq1, lq2, False, Pauli.PauliZ)
1650
+ if self.is_error_detection:
1651
+ self.h(lq2)
1652
+ self.cx(lq1, lq2)
1653
+ self.h(lq2)
1654
+ else:
1655
+ self._cpauli(lq1, lq2, False, Pauli.PauliZ)
1628
1656
 
1629
1657
  def acx(self, lq1, lq2):
1630
1658
  self._cpauli(lq1, lq2, True, Pauli.PauliX)
1631
1659
 
1632
1660
  def acy(self, lq1, lq2):
1633
- self._cpauli(lq1, lq2, True, Pauli.PauliY)
1661
+ if self.is_error_detection:
1662
+ self.adjs(lq2)
1663
+ self.acx(lq1, lq2)
1664
+ self.s(lq2)
1665
+ else:
1666
+ self._cpauli(lq1, lq2, True, Pauli.PauliY)
1634
1667
 
1635
1668
  def acz(self, lq1, lq2):
1636
- self._cpauli(lq1, lq2, True, Pauli.PauliZ)
1669
+ if self.is_error_detection:
1670
+ self.h(lq2)
1671
+ self.acx(lq1, lq2)
1672
+ self.h(lq2)
1673
+ else:
1674
+ self._cpauli(lq1, lq2, True, Pauli.PauliZ)
1637
1675
 
1638
1676
  def mcx(self, lq1, lq2):
1639
1677
  if len(lq1) > 1:
1640
1678
  raise RuntimeError(
1641
1679
  "QrackAceBackend.mcx() is provided for syntax convenience and only supports 1 control qubit!"
1642
1680
  )
1643
- self._cpauli(lq1[0], lq2, False, Pauli.PauliX)
1681
+ self.cx(lq1[0], lq2)
1644
1682
 
1645
1683
  def mcy(self, lq1, lq2):
1646
1684
  if len(lq1) > 1:
1647
1685
  raise RuntimeError(
1648
1686
  "QrackAceBackend.mcy() is provided for syntax convenience and only supports 1 control qubit!"
1649
1687
  )
1650
- self._cpauli(lq1[0], lq2, False, Pauli.PauliY)
1688
+ self.cy(lq1[0], lq2)
1651
1689
 
1652
1690
  def mcz(self, lq1, lq2):
1653
1691
  if len(lq1) > 1:
1654
1692
  raise RuntimeError(
1655
1693
  "QrackAceBackend.mcz() is provided for syntax convenience and only supports 1 control qubit!"
1656
1694
  )
1657
- self._cpauli(lq1[0], lq2, False, Pauli.PauliZ)
1695
+ self.cz(lq1[0], lq2)
1658
1696
 
1659
1697
  def macx(self, lq1, lq2):
1660
1698
  if len(lq1) > 1:
1661
1699
  raise RuntimeError(
1662
1700
  "QrackAceBackend.macx() is provided for syntax convenience and only supports 1 control qubit!"
1663
1701
  )
1664
- self._cpauli(lq1[0], lq2, True, Pauli.PauliX)
1702
+ self.acx(lq1[0], lq2)
1665
1703
 
1666
1704
  def macy(self, lq1, lq2):
1667
1705
  if len(lq1) > 1:
1668
1706
  raise RuntimeError(
1669
1707
  "QrackAceBackend.macy() is provided for syntax convenience and only supports 1 control qubit!"
1670
1708
  )
1671
- self._cpauli(lq1[0], lq2, True, Pauli.PauliY)
1709
+ self.acy(lq1[0], lq2)
1672
1710
 
1673
1711
  def macz(self, lq1, lq2):
1674
1712
  if len(lq1) > 1:
1675
1713
  raise RuntimeError(
1676
1714
  "QrackAceBackend.macz() is provided for syntax convenience and only supports 1 control qubit!"
1677
1715
  )
1678
- self._cpauli(lq1[0], lq2, True, Pauli.PauliZ)
1716
+ self.acz(lq1[0], lq2)
1679
1717
 
1680
1718
  def swap(self, lq1, lq2):
1681
1719
  hq1 = self._unpack(lq1)
@@ -1745,6 +1783,18 @@ class QrackAceBackend:
1745
1783
  # single-replica (bulk) qubits, so this is always safe to call, and
1746
1784
  # only actually matters (and was being silently skipped) whenever
1747
1785
  # either side is a multi-replica boundary qubit.
1786
+ #
1787
+ # Deliberately NOT wrapped with the same logical-level gadget
1788
+ # _cpauli uses below this branch: unlike a controlled-Pauli gate,
1789
+ # where the control is structurally invariant, SWAP genuinely,
1790
+ # intentionally exchanges both sides' values -- an independent
1791
+ # "did home1/home2 change" check would incorrectly flag a
1792
+ # legitimate swap as an error and try to veto it. The correct
1793
+ # invariant here is the COMBINED parity (home1 XOR home2), not
1794
+ # independent per-side checks, and getting that right when the
1795
+ # two sides can land on different simulators (the partial-match
1796
+ # branches below) needs separate, careful treatment -- not
1797
+ # reused as-is from _cpauli's helper.
1748
1798
  self._correct(lq1)
1749
1799
  self._correct(lq2)
1750
1800
 
@@ -1760,6 +1810,19 @@ class QrackAceBackend:
1760
1810
  # with or without this call).
1761
1811
  return
1762
1812
 
1813
+ anc1, anc2 = None, None
1814
+ if self.is_error_detection and not self._in_gadget_capture:
1815
+ if len(hq2) == 1:
1816
+ anc1 = self._detect_ancilla1_lq[hq1[0][0]]
1817
+ anc2 = self._detect_ancilla2_lq[hq1[0][0]]
1818
+ else:
1819
+ anc1 = self._detect_ancilla1_lq[hq2[0][0]]
1820
+ anc2 = self._detect_ancilla2_lq[hq2[0][0]]
1821
+ self._in_gadget_capture = True
1822
+ self.cx(lq1, anc1)
1823
+ self.cx(lq2, anc2)
1824
+ self._in_gadget_capture = False
1825
+
1763
1826
  # Partial-match cases: one side is a simple (single-replica) qubit,
1764
1827
  # the other has multiple replicas. For the multi-replica side's
1765
1828
  # non-matching replicas, the correct decomposition (verified
@@ -1791,9 +1854,7 @@ class QrackAceBackend:
1791
1854
  # error is already Z-basis-population-visible and doesn't need
1792
1855
  # an H-rotation first to become detectable.
1793
1856
  self._correct(lq2)
1794
- return
1795
-
1796
- if len(hq2) == 1 and any(h[0] == hq2[0][0] for h in hq1):
1857
+ elif len(hq2) == 1 and any(h[0] == hq2[0][0] for h in hq1):
1797
1858
  _hq2 = hq2[0]
1798
1859
  non_matching = [h for h in hq1 if h[0] != _hq2[0]]
1799
1860
  matching = [h for h in hq1 if h[0] == _hq2[0]]
@@ -1804,18 +1865,39 @@ class QrackAceBackend:
1804
1865
  for _hq1 in non_matching:
1805
1866
  self._cx_shadow(_hq2, _hq1)
1806
1867
  self._correct(lq1)
1807
- return
1808
-
1809
- # General case: no shared simulator at all (e.g. two simple
1810
- # qubits on entirely different sims -- nothing to anchor an exact
1811
- # swap on, verified the sandwich branches above give WRONG,
1812
- # deterministic results here: 100/100 failures vs 0/15 for this
1813
- # fallback), or both sides multi-replica and not fully matched:
1814
- # cx() already wraps itself with _correct() via _cpauli, so no
1815
- # extra wrapping needed here.
1816
- self.cx(lq1, lq2)
1817
- self.cx(lq2, lq1)
1818
- self.cx(lq1, lq2)
1868
+ else:
1869
+ # General case: no shared simulator at all (e.g. two simple
1870
+ # qubits on entirely different sims -- nothing to anchor an exact
1871
+ # swap on, verified the sandwich branches above give WRONG,
1872
+ # deterministic results here: 100/100 failures vs 0/15 for this
1873
+ # fallback), or both sides multi-replica and not fully matched:
1874
+ # cx() already wraps itself with _correct() via _cpauli, so no
1875
+ # extra wrapping needed here.
1876
+ self.cx(lq1, lq2)
1877
+ self.cx(lq2, lq1)
1878
+ self.cx(lq1, lq2)
1879
+
1880
+ if anc1 is not None:
1881
+ self._in_gadget_capture = True
1882
+ self.cx(lq1, anc2)
1883
+ self.cx(lq2, anc1)
1884
+ anc_sim, anc_idx = self._qubits[anc1][0]
1885
+ p = self.sim[anc_sim].prob(anc_idx)
1886
+ if p > 0.5:
1887
+ self.force_m(anc1, True)
1888
+ self.x(anc1)
1889
+ self.x(lq1)
1890
+ else:
1891
+ self.force_m(anc1, False)
1892
+ anc_sim, anc_idx = self._qubits[anc2][0]
1893
+ p = self.sim[anc_sim].prob(anc_idx)
1894
+ if p > 0.5:
1895
+ self.force_m(anc2, True)
1896
+ self.x(anc2)
1897
+ self.x(lq2)
1898
+ else:
1899
+ self.force_m(anc2, False)
1900
+ self._in_gadget_capture = False
1819
1901
 
1820
1902
  def iswap(self, lq1, lq2):
1821
1903
  self.swap(lq1, lq2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-complex128
3
- Version: 2.18.2
3
+ Version: 2.18.3
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -17,6 +17,9 @@ pyqrack/qrack_stabilizer.py
17
17
  pyqrack/quimb_circuit_type.py
18
18
  pyqrack/qrack_system/__init__.py
19
19
  pyqrack/qrack_system/qrack_system.py
20
+ pyqrack/qrack_system/qrack_cl_precompile/qrack_cl_precompile
21
+ pyqrack/qrack_system/qrack_lib/libqrack_pinvoke.so
22
+ pyqrack/qrack_system/qrack_lib/libqrack_pinvoke.so.10.13.0
20
23
  pyqrack/stats/__init__.py
21
24
  pyqrack/stats/load_quantized_data.py
22
25
  pyqrack/stats/quantize_by_range.py
@@ -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.18.2"
10
+ VERSION = "2.18.3"
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')