pyqrack-complex128 2.18.2__tar.gz → 2.18.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {pyqrack_complex128-2.18.2/pyqrack_complex128.egg-info → pyqrack_complex128-2.18.4}/PKG-INFO +1 -1
  2. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_ace_backend.py +286 -205
  3. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4/pyqrack_complex128.egg-info}/PKG-INFO +1 -1
  4. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/setup.py +1 -1
  5. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/LICENSE +0 -0
  6. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/MANIFEST.in +0 -0
  7. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/Makefile +0 -0
  8. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/README.md +0 -0
  9. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyproject.toml +0 -0
  10. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/__init__.py +0 -0
  11. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/neuron_activation_fn.py +0 -0
  12. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/pauli.py +0 -0
  13. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_circuit.py +0 -0
  14. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  15. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_neuron.py +0 -0
  16. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  17. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_simulator.py +0 -0
  18. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_stabilizer.py +0 -0
  19. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_system/__init__.py +0 -0
  20. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/qrack_system/qrack_system.py +0 -0
  21. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/quimb_circuit_type.py +0 -0
  22. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/stats/__init__.py +0 -0
  23. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/stats/load_quantized_data.py +0 -0
  24. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack/stats/quantize_by_range.py +0 -0
  25. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack_complex128.egg-info/SOURCES.txt +0 -0
  26. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack_complex128.egg-info/dependency_links.txt +0 -0
  27. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack_complex128.egg-info/not-zip-safe +0 -0
  28. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack_complex128.egg-info/requires.txt +0 -0
  29. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/pyqrack_complex128.egg-info/top_level.txt +0 -0
  30. {pyqrack_complex128-2.18.2 → pyqrack_complex128-2.18.4}/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.4
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)
@@ -1179,9 +1179,7 @@ class QrackAceBackend:
1179
1179
  self.sim[b[0]].h(b[1])
1180
1180
  return
1181
1181
 
1182
- self._correct(lq)
1183
1182
  skip = self._invalidate_for_gate(lq)
1184
-
1185
1183
  qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
1186
1184
 
1187
1185
  for q in qb:
@@ -1444,6 +1442,56 @@ class QrackAceBackend:
1444
1442
  # bounded window, no reactive invalidation
1445
1443
  # needed for these -- see
1446
1444
  # _resolve_witnessed_shadow.
1445
+
1446
+ # Logical-level error-detection gadget: captures lq1's LOGICAL
1447
+ # value (and lq2's too, for CZ specifically -- see below) into a
1448
+ # dedicated hidden ancilla via a genuine, nested cx() call --
1449
+ # NOT a raw capture of one specific physical replica. This
1450
+ # matters: gate_fn/shadow_fn below never gate the control (only
1451
+ # ever read it via .prob()), so lq1's LOGICAL value is
1452
+ # structurally guaranteed not to change across this loop --
1453
+ # that invariant holds regardless of what _correct() may have
1454
+ # legitimately done to any INDIVIDUAL replica beforehand,
1455
+ # because the nested cx(lq1, anc) call determines lq1's value
1456
+ # the same robust way any other logical coupling would, not by
1457
+ # trusting one physical replica directly (an earlier version of
1458
+ # this tried exactly that and crashed: _correct() can
1459
+ # legitimately need to change hq1[0]'s own value, so "hq1[0]
1460
+ # never changes" isn't actually a valid invariant, even though
1461
+ # "lq1's logical value never changes here" is).
1462
+ #
1463
+ # The genuine syndrome signal comes from the capture itself: the
1464
+ # nested cx() goes through the same _correct()/shadow-coupling
1465
+ # machinery as any other logical operation, which can introduce
1466
+ # some imprecision between the "before" and "after" capture even
1467
+ # though lq1's true value didn't change -- that imprecision is
1468
+ # exactly this architecture's own intrinsic (not noise=p)
1469
+ # approximation error, which is what's worth catching.
1470
+ #
1471
+ # CZ also captures lq2 into the SAME ancilla: unlike CX/CY,
1472
+ # where the target's value legitimately, intentionally changes
1473
+ # (that's the entangling operation, not an error), CZ never
1474
+ # changes either participant's Z-population on any input, so
1475
+ # lq2's logical value is an equally valid invariant here, for
1476
+ # this gate family only -- same reasoning as the earlier
1477
+ # single-replica CZ dual-check, just applied at the logical
1478
+ # level now.
1479
+ anc1, anc2 = None, None
1480
+ if self.is_error_detection and not self._in_gadget_capture and ((len(hq1) > 1) or (len(hq2) > 1)):
1481
+ anc1 = self._detect_ancilla1_lq[hq1[0][0]]
1482
+ self._in_gadget_capture = True
1483
+ # Control bit-flip
1484
+ self.cx(lq1, anc1)
1485
+ if len(hq1) > 1:
1486
+ anc2 = self._detect_ancilla2_lq[hq1[0][0]]
1487
+ # XOR on target
1488
+ self.cx(lq2, anc2)
1489
+ if anti:
1490
+ self.acx(lq1, anc2)
1491
+ else:
1492
+ self.cx(lq1, anc2)
1493
+ self._in_gadget_capture = False
1494
+
1447
1495
  for q1 in qb1:
1448
1496
  b1 = hq1[q1]
1449
1497
  gate_fn, shadow_fn = self._get_gate(pauli, anti, b1[0])
@@ -1455,88 +1503,56 @@ class QrackAceBackend:
1455
1503
  for q2 in qb2:
1456
1504
  b2 = hq2[q2]
1457
1505
  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])
1506
+ gate_fn([b1[1]], b2[1])
1534
1507
  elif lq1_lr or (b1[1] == b2[1]) or ((len(qb1) == 2) and (b1[1] == (b2[1] & 1))):
1535
1508
  shadow_fn(b1, b2)
1536
1509
  shadow_targets.append(b2)
1537
1510
  if witness is not None and witness != b2:
1538
1511
  witnessed_targets.append((b2, witness))
1539
1512
 
1513
+ if anc1 is not None:
1514
+ self._in_gadget_capture = True
1515
+ self.cx(lq1, anc1)
1516
+ # Check prob() before forcing: force_m(anc, False)
1517
+ # assumes the "agree" branch has nonzero probability,
1518
+ # which usually holds (that's the whole invariant this
1519
+ # gadget relies on) but isn't guaranteed -- if the
1520
+ # architecture's own approximation genuinely, confidently
1521
+ # drove lq1 (or lq2) to a different value between the
1522
+ # two captures, the "agree" branch can have exactly zero
1523
+ # probability, and forcing it crashes outright (verified
1524
+ # directly: reproducible RuntimeError under realistic
1525
+ # circuit depth). Accept that a genuine, confident
1526
+ # disagreement occurred instead of forcing the
1527
+ # impossible branch: force the TRUE condition (safe,
1528
+ # since that branch is the one that actually carries the
1529
+ # probability), reset the ancilla back to |0>, and apply
1530
+ # a corrective X to lq1 to restore the detected
1531
+ # invariant.
1532
+ anc_sim, anc_idx = self._qubits[anc1][0]
1533
+ p = self.sim[anc_sim].prob(anc_idx)
1534
+ if p > 0.5:
1535
+ self.force_m(anc1, True)
1536
+ self.x(anc1)
1537
+ self.x(lq1)
1538
+ else:
1539
+ self.force_m(anc1, False)
1540
+ self._in_gadget_capture = False
1541
+
1542
+ if anc2 is not None:
1543
+ self._in_gadget_capture = True
1544
+ # XOR check
1545
+ self.cx(lq2, anc2)
1546
+ anc_sim, anc_idx = self._qubits[anc2][0]
1547
+ p = self.sim[anc_sim].prob(anc_idx)
1548
+ if p > 0.5:
1549
+ self.force_m(anc2, True)
1550
+ self.x(anc2)
1551
+ self.x(lq2)
1552
+ else:
1553
+ self.force_m(anc2, False)
1554
+ self._in_gadget_capture = False
1555
+
1540
1556
  if lq2 is not None and witnessed_targets and self._witness_map is not None:
1541
1557
  wmap = self._witness_map.setdefault(lq2, {})
1542
1558
  for target_replica, witness_replica in witnessed_targets:
@@ -1621,61 +1637,81 @@ class QrackAceBackend:
1621
1637
  self._cpauli(lq1, lq2, False, Pauli.PauliX)
1622
1638
 
1623
1639
  def cy(self, lq1, lq2):
1624
- self._cpauli(lq1, lq2, False, Pauli.PauliY)
1640
+ if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1641
+ self.adjs(lq2)
1642
+ self.cx(lq1, lq2)
1643
+ self.s(lq2)
1644
+ else:
1645
+ self._cpauli(lq1, lq2, False, Pauli.PauliY)
1625
1646
 
1626
1647
  def cz(self, lq1, lq2):
1627
- self._cpauli(lq1, lq2, False, Pauli.PauliZ)
1648
+ if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1649
+ self.h(lq2)
1650
+ self.cx(lq1, lq2)
1651
+ self.h(lq2)
1652
+ else:
1653
+ self._cpauli(lq1, lq2, False, Pauli.PauliZ)
1628
1654
 
1629
1655
  def acx(self, lq1, lq2):
1630
1656
  self._cpauli(lq1, lq2, True, Pauli.PauliX)
1631
1657
 
1632
1658
  def acy(self, lq1, lq2):
1633
- self._cpauli(lq1, lq2, True, Pauli.PauliY)
1659
+ if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1660
+ self.adjs(lq2)
1661
+ self.acx(lq1, lq2)
1662
+ self.s(lq2)
1663
+ else:
1664
+ self._cpauli(lq1, lq2, True, Pauli.PauliY)
1634
1665
 
1635
1666
  def acz(self, lq1, lq2):
1636
- self._cpauli(lq1, lq2, True, Pauli.PauliZ)
1667
+ if self.is_error_detection and ((len(self._unpack(lq1)) > 1) or (len(self._unpack(lq2)) > 1)):
1668
+ self.h(lq2)
1669
+ self.acx(lq1, lq2)
1670
+ self.h(lq2)
1671
+ else:
1672
+ self._cpauli(lq1, lq2, True, Pauli.PauliZ)
1637
1673
 
1638
1674
  def mcx(self, lq1, lq2):
1639
1675
  if len(lq1) > 1:
1640
1676
  raise RuntimeError(
1641
1677
  "QrackAceBackend.mcx() is provided for syntax convenience and only supports 1 control qubit!"
1642
1678
  )
1643
- self._cpauli(lq1[0], lq2, False, Pauli.PauliX)
1679
+ self.cx(lq1[0], lq2)
1644
1680
 
1645
1681
  def mcy(self, lq1, lq2):
1646
1682
  if len(lq1) > 1:
1647
1683
  raise RuntimeError(
1648
1684
  "QrackAceBackend.mcy() is provided for syntax convenience and only supports 1 control qubit!"
1649
1685
  )
1650
- self._cpauli(lq1[0], lq2, False, Pauli.PauliY)
1686
+ self.cy(lq1[0], lq2)
1651
1687
 
1652
1688
  def mcz(self, lq1, lq2):
1653
1689
  if len(lq1) > 1:
1654
1690
  raise RuntimeError(
1655
1691
  "QrackAceBackend.mcz() is provided for syntax convenience and only supports 1 control qubit!"
1656
1692
  )
1657
- self._cpauli(lq1[0], lq2, False, Pauli.PauliZ)
1693
+ self.cz(lq1[0], lq2)
1658
1694
 
1659
1695
  def macx(self, lq1, lq2):
1660
1696
  if len(lq1) > 1:
1661
1697
  raise RuntimeError(
1662
1698
  "QrackAceBackend.macx() is provided for syntax convenience and only supports 1 control qubit!"
1663
1699
  )
1664
- self._cpauli(lq1[0], lq2, True, Pauli.PauliX)
1700
+ self.acx(lq1[0], lq2)
1665
1701
 
1666
1702
  def macy(self, lq1, lq2):
1667
1703
  if len(lq1) > 1:
1668
1704
  raise RuntimeError(
1669
1705
  "QrackAceBackend.macy() is provided for syntax convenience and only supports 1 control qubit!"
1670
1706
  )
1671
- self._cpauli(lq1[0], lq2, True, Pauli.PauliY)
1707
+ self.acy(lq1[0], lq2)
1672
1708
 
1673
1709
  def macz(self, lq1, lq2):
1674
1710
  if len(lq1) > 1:
1675
1711
  raise RuntimeError(
1676
1712
  "QrackAceBackend.macz() is provided for syntax convenience and only supports 1 control qubit!"
1677
1713
  )
1678
- self._cpauli(lq1[0], lq2, True, Pauli.PauliZ)
1714
+ self.acz(lq1[0], lq2)
1679
1715
 
1680
1716
  def swap(self, lq1, lq2):
1681
1717
  hq1 = self._unpack(lq1)
@@ -1745,6 +1781,18 @@ class QrackAceBackend:
1745
1781
  # single-replica (bulk) qubits, so this is always safe to call, and
1746
1782
  # only actually matters (and was being silently skipped) whenever
1747
1783
  # either side is a multi-replica boundary qubit.
1784
+ #
1785
+ # Deliberately NOT wrapped with the same logical-level gadget
1786
+ # _cpauli uses below this branch: unlike a controlled-Pauli gate,
1787
+ # where the control is structurally invariant, SWAP genuinely,
1788
+ # intentionally exchanges both sides' values -- an independent
1789
+ # "did home1/home2 change" check would incorrectly flag a
1790
+ # legitimate swap as an error and try to veto it. The correct
1791
+ # invariant here is the COMBINED parity (home1 XOR home2), not
1792
+ # independent per-side checks, and getting that right when the
1793
+ # two sides can land on different simulators (the partial-match
1794
+ # branches below) needs separate, careful treatment -- not
1795
+ # reused as-is from _cpauli's helper.
1748
1796
  self._correct(lq1)
1749
1797
  self._correct(lq2)
1750
1798
 
@@ -1760,6 +1808,19 @@ class QrackAceBackend:
1760
1808
  # with or without this call).
1761
1809
  return
1762
1810
 
1811
+ anc1, anc2 = None, None
1812
+ if self.is_error_detection and not self._in_gadget_capture and ((len(hq1) > 1) or (len(hq2) > 1)):
1813
+ if len(hq2) == 1:
1814
+ anc1 = self._detect_ancilla1_lq[hq1[0][0]]
1815
+ anc2 = self._detect_ancilla2_lq[hq1[0][0]]
1816
+ else:
1817
+ anc1 = self._detect_ancilla1_lq[hq2[0][0]]
1818
+ anc2 = self._detect_ancilla2_lq[hq2[0][0]]
1819
+ self._in_gadget_capture = True
1820
+ self.cx(lq1, anc1)
1821
+ self.cx(lq2, anc2)
1822
+ self._in_gadget_capture = False
1823
+
1763
1824
  # Partial-match cases: one side is a simple (single-replica) qubit,
1764
1825
  # the other has multiple replicas. For the multi-replica side's
1765
1826
  # non-matching replicas, the correct decomposition (verified
@@ -1791,9 +1852,7 @@ class QrackAceBackend:
1791
1852
  # error is already Z-basis-population-visible and doesn't need
1792
1853
  # an H-rotation first to become detectable.
1793
1854
  self._correct(lq2)
1794
- return
1795
-
1796
- if len(hq2) == 1 and any(h[0] == hq2[0][0] for h in hq1):
1855
+ elif len(hq2) == 1 and any(h[0] == hq2[0][0] for h in hq1):
1797
1856
  _hq2 = hq2[0]
1798
1857
  non_matching = [h for h in hq1 if h[0] != _hq2[0]]
1799
1858
  matching = [h for h in hq1 if h[0] == _hq2[0]]
@@ -1804,18 +1863,39 @@ class QrackAceBackend:
1804
1863
  for _hq1 in non_matching:
1805
1864
  self._cx_shadow(_hq2, _hq1)
1806
1865
  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)
1866
+ else:
1867
+ # General case: no shared simulator at all (e.g. two simple
1868
+ # qubits on entirely different sims -- nothing to anchor an exact
1869
+ # swap on, verified the sandwich branches above give WRONG,
1870
+ # deterministic results here: 100/100 failures vs 0/15 for this
1871
+ # fallback), or both sides multi-replica and not fully matched:
1872
+ # cx() already wraps itself with _correct() via _cpauli, so no
1873
+ # extra wrapping needed here.
1874
+ self.cx(lq1, lq2)
1875
+ self.cx(lq2, lq1)
1876
+ self.cx(lq1, lq2)
1877
+
1878
+ if anc1 is not None:
1879
+ self._in_gadget_capture = True
1880
+ self.cx(lq1, anc2)
1881
+ self.cx(lq2, anc1)
1882
+ anc_sim, anc_idx = self._qubits[anc1][0]
1883
+ p = self.sim[anc_sim].prob(anc_idx)
1884
+ if p > 0.5:
1885
+ self.force_m(anc1, True)
1886
+ self.x(anc1)
1887
+ self.x(lq1)
1888
+ else:
1889
+ self.force_m(anc1, False)
1890
+ anc_sim, anc_idx = self._qubits[anc2][0]
1891
+ p = self.sim[anc_sim].prob(anc_idx)
1892
+ if p > 0.5:
1893
+ self.force_m(anc2, True)
1894
+ self.x(anc2)
1895
+ self.x(lq2)
1896
+ else:
1897
+ self.force_m(anc2, False)
1898
+ self._in_gadget_capture = False
1819
1899
 
1820
1900
  def iswap(self, lq1, lq2):
1821
1901
  self.swap(lq1, lq2)
@@ -2431,35 +2511,32 @@ class QrackAceBackend:
2431
2511
 
2432
2512
  coupling_map = self.get_logical_coupling_map()
2433
2513
 
2434
- # Error-detection damping (cx/cy/cz only -- see below for why not
2435
- # swap/iswap): a qubit only ever gets the gadget's benefit when it
2436
- # actually plays CONTROL in some real (u=0) gate somewhere in the
2437
- # coupling map -- that's what gives its replica a chance to be
2438
- # swept clean via _apply_coupling's capture-sandwich-force_m
2439
- # before any shadow decision reads its .prob(). This is a
2440
- # structural fact about each qubit individually, computed once
2441
- # here, not a property of the circuit or a global constant --
2442
- # which is exactly why this can't be reproduced by a user just
2443
- # passing a smaller x: x scales every pair uniformly, and this
2444
- # doesn't apply uniformly, either across qubits (only ones with a
2445
- # real-gate opportunity qualify) or across gate types (see below).
2446
- #
2447
- # Both sides of a real (u=0) pair get added, not just the control:
2448
- # CZ's own gadget check now covers its target too (CZ is diagonal
2449
- # -- unlike CX/CY, it never changes either qubit's Z-population,
2450
- # so the target is an equally valid invariance check there, see
2451
- # _apply_coupling). The coupling map doesn't distinguish which
2452
- # gate type a future circuit will actually use for a given pair,
2453
- # so this necessarily overstates the CX/CY case slightly (where
2454
- # only the control gets swept) -- an approximation on top of an
2455
- # already-approximate placeholder, not a new source of precision
2456
- # lost.
2457
- has_real_partner = set()
2458
- if self.is_error_detection:
2459
- for lq1, lq2 in coupling_map:
2460
- if _uncommon_sim_fraction(lq1, lq2) == 0:
2461
- has_real_partner.add(lq1)
2462
- has_real_partner.add(lq2)
2514
+ # Error-detection damping (cx/cy/cz; swap/iswap handled
2515
+ # separately below): the current gadget structurally requires
2516
+ # lq1 (== "a" for a given coupling_map entry, since a always
2517
+ # plays the control's role for that entry -- confirmed against
2518
+ # _cpauli's own convention) to be a multi-replica (boundary)
2519
+ # qubit -- see _cpauli: anc1 (the control-invariance capture) is
2520
+ # unconditional but structurally trivial for a single-replica
2521
+ # control (_correct() is already a no-op there, nothing for the
2522
+ # gadget to protect against), and anc2 (the target-value
2523
+ # verification against the CX/ACX-predicted output) is
2524
+ # explicitly gated on len(hq1) > 1. Both halves are gated the
2525
+ # same, simple way: whether a itself is boundary. This replaces
2526
+ # an older version keyed on _uncommon_sim_fraction(lq1,lq2)==0
2527
+ # ("does this pair share a simulator"), which matched an EARLIER
2528
+ # gadget that wrapped gate_fn alone and only ever ran on the
2529
+ # real (same-sim) branch -- that gadget was subsequently proven
2530
+ # a mathematically guaranteed no-op (gate_fn structurally never
2531
+ # touches its control's Z-value regardless of the control's
2532
+ # incoming state, verified directly to floating-point
2533
+ # precision) and replaced with the current mechanism, which
2534
+ # doesn't care whether a and b share a simulator at all: the
2535
+ # nested cx(lq1, anc) capture goes through the full logical
2536
+ # coupling machinery (shadow or real, whichever applies) and
2537
+ # works the same way regardless. So this is checked directly,
2538
+ # per pair, from len(self._qubits[a]) -- no precomputed set
2539
+ # needed.
2463
2540
 
2464
2541
  # This factor is a theory-derived placeholder, not an empirical
2465
2542
  # fit -- deliberately, per instruction not to chase a regression
@@ -2503,13 +2580,13 @@ class QrackAceBackend:
2503
2580
  # Pauli-string convention is big-endian relative to the qubit
2504
2581
  # list: the LAST qubit corresponds to the LEFTMOST character).
2505
2582
  p_shadow_ccz = 1 - sp * p
2506
- if a in has_real_partner:
2583
+ if self.is_error_detection and len(self._qubits[a]) > 1:
2507
2584
  # a is the control for this pair (_cpauli(lq1=a, lq2=b,
2508
- # ...) is always called with a as the coupling source),
2509
- # and a has at least one real-gate opportunity elsewhere
2510
- # in the map -- so its .prob() reading, which THIS
2511
- # shadow decision partly depends on, has a structural
2512
- # chance of having been kept clean. Damp accordingly.
2585
+ # ...) is always called with a as the coupling source).
2586
+ # Both gadget halves that protect this pair (anc1 on a,
2587
+ # anc2 verifying b) are structurally live exactly when a
2588
+ # itself is boundary -- see the comment above this loop.
2589
+ # Damp accordingly.
2513
2590
  p_shadow_ccz *= y
2514
2591
  noise_model.add_quantum_error(
2515
2592
  pauli_error([("ZI", p_shadow_ccz), ("II", 1 - p_shadow_ccz)]), "cz", [a, b]
@@ -2563,21 +2640,22 @@ class QrackAceBackend:
2563
2640
  # same condition to stay consistent with what the gate does.
2564
2641
  if (is_a_simple != is_b_simple) and has_match:
2565
2642
  p_net_swap = 2 * p * (1 - p)
2566
- # swap() now routes its own matching-replica gate through
2567
- # _detect_swap (a genuine same-simulator, parity-invariant
2568
- # gadget: idx1^idx2 is exactly conserved across a SWAP,
2569
- # the same way a CNOT's control is conserved -- see
2570
- # _detect_swap), so this IS damped now, unlike the stale
2571
- # "swap/iswap deliberately not damped" note this replaced.
2572
- # Gate on whichever side bears the error (the boundary
2573
- # qubit), using the same has_real_partner structural
2574
- # proxy as the cx/cy/cz case above -- the matching-replica
2575
- # branch needs the same "shares a simulator with
2576
- # something else" precondition _uncommon_sim_fraction==0
2577
- # already captures, so reusing that set here keeps this
2578
- # consistent rather than re-deriving a parallel condition.
2579
- boundary_qubit = b if is_a_simple else a
2580
- if boundary_qubit in has_real_partner:
2643
+ # swap()'s own anc1/anc2 cross-check (verifying lq1's new
2644
+ # value against lq2's old, and vice versa -- see swap())
2645
+ # wraps the ENTIRE call, every branch, not specifically
2646
+ # this matching-replica step -- _detect_swap itself is
2647
+ # just a plain swap(), carrying no gadget of its own
2648
+ # (an earlier per-replica parity gadget living there was
2649
+ # proven a mathematically guaranteed no-op, the same way
2650
+ # the old cx/cy/cz gadget was, and was simplified away).
2651
+ # So this IS damped, unlike the stale "swap/iswap
2652
+ # deliberately not damped" note this replaced -- but
2653
+ # unconditionally so, not gated on any structural
2654
+ # per-qubit condition: swap()'s gadget captures both
2655
+ # sides whenever is_error_detection is on, full stop, no
2656
+ # len(hq)>1-style precondition the way _cpauli's anc2
2657
+ # has.
2658
+ if self.is_error_detection:
2581
2659
  p_net_swap *= y
2582
2660
  if is_a_simple:
2583
2661
  # a is bulk, b is boundary -> error lands on b
@@ -2610,12 +2688,15 @@ class QrackAceBackend:
2610
2688
  # previous formula.
2611
2689
  if (is_a_simple != is_b_simple) and has_match:
2612
2690
  p_cz = 1 - sp * p # same per-call formula as standalone cz
2613
- if boundary_qubit in has_real_partner:
2614
- # this cz component is the SAME kind of shadow-driven
2615
- # term as the main cx/cy/cz loop above (see that
2616
- # comment for the mechanism); p_net_swap was already
2617
- # damped above, independently, since it comes from a
2618
- # different gate (the now-protected native swap, not
2691
+ if self.is_error_detection and len(self._qubits[a]) > 1:
2692
+ # this cz component is routed through cx() when
2693
+ # is_error_detection is on (see cz()'s own wrapper),
2694
+ # so it's the SAME kind of shadow-driven term, gated
2695
+ # the SAME way, as the main cx/cy/cz loop above (see
2696
+ # that comment for the mechanism); p_net_swap was
2697
+ # already damped above, independently, since it comes
2698
+ # from a different gate (the now-protected native
2699
+ # swap, not
2619
2700
  # this cz component).
2620
2701
  p_cz *= y
2621
2702
  p_i = (1 - p_net_swap) * (1 - p_cz)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-complex128
3
- Version: 2.18.2
3
+ Version: 2.18.4
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -7,7 +7,7 @@ from setuptools import setup
7
7
  from setuptools.command.build_py import build_py
8
8
 
9
9
 
10
- VERSION = "2.18.2"
10
+ VERSION = "2.18.4"
11
11
 
12
12
  # Read long description from README.
13
13
  README_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'README.md')