pyqrack-cpu 2.18.0__tar.gz → 2.18.2__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-2.18.0/pyqrack_cpu.egg-info → pyqrack_cpu-2.18.2}/PKG-INFO +1 -1
  2. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_ace_backend.py +203 -45
  3. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2/pyqrack_cpu.egg-info}/PKG-INFO +1 -1
  4. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/setup.py +1 -1
  5. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/LICENSE +0 -0
  6. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/MANIFEST.in +0 -0
  7. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/Makefile +0 -0
  8. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/README.md +0 -0
  9. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyproject.toml +0 -0
  10. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/__init__.py +0 -0
  11. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/neuron_activation_fn.py +0 -0
  12. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/pauli.py +0 -0
  13. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_circuit.py +0 -0
  14. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  15. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_neuron.py +0 -0
  16. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  17. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_simulator.py +0 -0
  18. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_stabilizer.py +0 -0
  19. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_system/__init__.py +0 -0
  20. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/qrack_system/qrack_system.py +0 -0
  21. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/quimb_circuit_type.py +0 -0
  22. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/stats/__init__.py +0 -0
  23. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/stats/load_quantized_data.py +0 -0
  24. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack/stats/quantize_by_range.py +0 -0
  25. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack_cpu.egg-info/SOURCES.txt +0 -0
  26. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack_cpu.egg-info/dependency_links.txt +0 -0
  27. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack_cpu.egg-info/not-zip-safe +0 -0
  28. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack_cpu.egg-info/requires.txt +0 -0
  29. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/pyqrack_cpu.egg-info/top_level.txt +0 -0
  30. {pyqrack_cpu-2.18.0 → pyqrack_cpu-2.18.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu
3
- Version: 2.18.0
3
+ Version: 2.18.2
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -631,10 +631,55 @@ class QrackAceBackend:
631
631
  else:
632
632
  t.adjs()
633
633
 
634
- def _anti_cz_shadow(self, c, t):
634
+ 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)
655
+
656
+ 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)
635
675
  self._qec_x(c)
636
- self._cz_shadow(c, t)
676
+ middle_fn(c, t)
637
677
  self._qec_x(c)
678
+ sim.mcx([c[1]], anc)
679
+ sim.force_m(anc, False)
680
+
681
+ def _anti_cz_shadow(self, c, t):
682
+ self._anti_shadow_wrap(c, t, self._cz_shadow)
638
683
 
639
684
  def _cx_shadow(self, c, t):
640
685
  self._qec_h(t)
@@ -642,9 +687,7 @@ class QrackAceBackend:
642
687
  self._qec_h(t)
643
688
 
644
689
  def _anti_cx_shadow(self, c, t):
645
- self._qec_x(c)
646
- self._cx_shadow(c, t)
647
- self._qec_x(c)
690
+ self._anti_shadow_wrap(c, t, self._cx_shadow)
648
691
 
649
692
  def _cy_shadow(self, c, t):
650
693
  self._qec_adjs(t)
@@ -652,9 +695,7 @@ class QrackAceBackend:
652
695
  self._qec_s(t)
653
696
 
654
697
  def _anti_cy_shadow(self, c, t):
655
- self._qec_x(c)
656
- self._cy_shadow(c, t)
657
- self._qec_x(c)
698
+ self._anti_shadow_wrap(c, t, self._cy_shadow)
658
699
 
659
700
  def _unpack(self, lq):
660
701
  return self._qubits[lq]
@@ -1415,44 +1456,78 @@ class QrackAceBackend:
1415
1456
  b2 = hq2[q2]
1416
1457
  if b1[0] == b2[0]:
1417
1458
  if self.is_error_detection:
1418
- # Error-DETECTION gadget (not correction), directly
1419
- # post-selected via force_m rather than requiring
1420
- # real repeated shots: b1 is always the CONTROL of
1421
- # this gate (by construction -- gate_fn is always
1422
- # called as gate_fn([b1[1]], b2[1])), and a
1423
- # control's Z value is exactly invariant under a
1424
- # controlled-Pauli gate in the noiseless case,
1425
- # regardless of anti/regular control convention or
1426
- # which Pauli. So: XOR b1's value into the patch's
1427
- # shared ancilla immediately before the gate, XOR
1428
- # it in again immediately after -- if nothing
1429
- # disturbed b1 (no injected noise on the gate
1430
- # itself or on either sandwiching CNOT), those two
1431
- # XORs exactly cancel and the ancilla is back to
1432
- # |0>. force_m(ancilla, False) then directly
1433
- # projects the whole simulated state onto that
1434
- # "no disturbance detected" branch and renormalizes
1435
- # -- this is the exact numerical equivalent of what
1436
- # a real experiment gets from measure + discard +
1437
- # repeat, done in one step because the simulator
1438
- # has the full state to project, not just samples
1439
- # from it. The same ancilla is reused for every
1440
- # gate touching this simulator: each round-trip is
1441
- # self-contained and leaves the ancilla at |0>
1442
- # again whenever nothing was detected, so there's
1443
- # no cross-gate bookkeeping needed.
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.
1444
1478
  #
1445
- # Scope: this only covers the mcx/mcy/mcz path
1446
- # (_cpauli / _apply_coupling). swap/iswap/pswap/etc.
1447
- # exchange both qubits' values rather than leaving
1448
- # either invariant, so this specific gadget doesn't
1449
- # apply to them directly -- left as a documented
1450
- # gap for a later pass, not silently unhandled.
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
1451
1522
  anc = self._detect_ancilla[b1[0]]
1452
1523
  sim = self.sim[b1[0]]
1453
1524
  sim.mcx([b1[1]], anc)
1525
+ if check_b2_too:
1526
+ sim.mcx([b2[1]], anc)
1454
1527
  gate_fn([b1[1]], b2[1])
1455
1528
  sim.mcx([b1[1]], anc)
1529
+ if check_b2_too:
1530
+ sim.mcx([b2[1]], anc)
1456
1531
  sim.force_m(anc, False)
1457
1532
  else:
1458
1533
  gate_fn([b1[1]], b2[1])
@@ -1677,7 +1752,7 @@ class QrackAceBackend:
1677
1752
  for i in range(len(hq1)):
1678
1753
  sim_id, idx1 = hq1[i]
1679
1754
  _, idx2 = hq2[i]
1680
- self.sim[sim_id].swap(idx1, idx2)
1755
+ self._detect_swap(sim_id, idx1, idx2)
1681
1756
  # No post-swap correction: this branch is a native index
1682
1757
  # relabeling applied identically to every matching replica
1683
1758
  # pair, with zero Schmidt-truncation opportunity -- verified
@@ -1706,7 +1781,7 @@ class QrackAceBackend:
1706
1781
  for _hq2 in non_matching:
1707
1782
  self._cx_shadow(_hq1, _hq2)
1708
1783
  for _hq2 in matching:
1709
- self.sim[_hq1[0]].swap(_hq1[1], _hq2[1])
1784
+ self._detect_swap(_hq1[0], _hq1[1], _hq2[1])
1710
1785
  for _hq2 in non_matching:
1711
1786
  self._cx_shadow(_hq1, _hq2)
1712
1787
  # No correction on lq1 (bulk, single-replica): already a
@@ -1725,7 +1800,7 @@ class QrackAceBackend:
1725
1800
  for _hq1 in non_matching:
1726
1801
  self._cx_shadow(_hq2, _hq1)
1727
1802
  for _hq1 in matching:
1728
- self.sim[_hq2[0]].swap(_hq2[1], _hq1[1])
1803
+ self._detect_swap(_hq2[0], _hq2[1], _hq1[1])
1729
1804
  for _hq1 in non_matching:
1730
1805
  self._cx_shadow(_hq2, _hq1)
1731
1806
  self._correct(lq1)
@@ -2334,7 +2409,7 @@ class QrackAceBackend:
2334
2409
  return self._coupling_map
2335
2410
 
2336
2411
  # Designed by Dan, Elara (ChatGPT), and (Anthropic) Claude:
2337
- def create_noise_model(self, x=0.5):
2412
+ def create_noise_model(self, x=0.5, y=0.5):
2338
2413
  if not _IS_QISKIT_AER_AVAILABLE:
2339
2414
  raise RuntimeError(
2340
2415
  "Before trying to run_qiskit_circuit() with QrackAceBackend, you must install Qiskit Aer!"
@@ -2354,7 +2429,58 @@ class QrackAceBackend:
2354
2429
 
2355
2430
  return n / (len(sims1) + len(sims2))
2356
2431
 
2357
- for a, b in self.get_logical_coupling_map():
2432
+ coupling_map = self.get_logical_coupling_map()
2433
+
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)
2463
+
2464
+ # This factor is a theory-derived placeholder, not an empirical
2465
+ # fit -- deliberately, per instruction not to chase a regression
2466
+ # right now. It reflects that the gadget's control-invariance
2467
+ # check is a Z-basis check: it exactly catches the X/Y-type
2468
+ # disturbances that would degrade a Z-basis .prob() reading (the
2469
+ # only thing _cz_shadow's decision actually depends on), while a
2470
+ # Z-type disturbance on the control doesn't degrade that reading
2471
+ # in the first place and so was never a threat to the shadow
2472
+ # decision to begin with. That argument alone would suggest
2473
+ # damping close to the full X/Y-type share of the channel -- but
2474
+ # this is a STRUCTURAL possibility (the control CAN be swept
2475
+ # clean somewhere in the map), not a per-execution guarantee that
2476
+ # it WAS, before any specific shadow read in a specific circuit.
2477
+ # 0.5 is a deliberately conservative middle estimate given that
2478
+ # gap, not a derived bound; replace with a fitted value once (or
2479
+ # if) empirical calibration against real XEB data happens.
2480
+ # _ERROR_DETECTION_DAMPING = 0.5
2481
+ # (Use "y" instead.)
2482
+
2483
+ for a, b in coupling_map:
2358
2484
  u = _uncommon_sim_fraction(a, b)
2359
2485
  c = 1 - u
2360
2486
 
@@ -2377,6 +2503,14 @@ class QrackAceBackend:
2377
2503
  # Pauli-string convention is big-endian relative to the qubit
2378
2504
  # list: the LAST qubit corresponds to the LEFTMOST character).
2379
2505
  p_shadow_ccz = 1 - sp * p
2506
+ if a in has_real_partner:
2507
+ # 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.
2513
+ p_shadow_ccz *= y
2380
2514
  noise_model.add_quantum_error(
2381
2515
  pauli_error([("ZI", p_shadow_ccz), ("II", 1 - p_shadow_ccz)]), "cz", [a, b]
2382
2516
  )
@@ -2429,6 +2563,22 @@ class QrackAceBackend:
2429
2563
  # same condition to stay consistent with what the gate does.
2430
2564
  if (is_a_simple != is_b_simple) and has_match:
2431
2565
  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:
2581
+ p_net_swap *= y
2432
2582
  if is_a_simple:
2433
2583
  # a is bulk, b is boundary -> error lands on b
2434
2584
  noise_model.add_quantum_error(
@@ -2460,6 +2610,14 @@ class QrackAceBackend:
2460
2610
  # previous formula.
2461
2611
  if (is_a_simple != is_b_simple) and has_match:
2462
2612
  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
2619
+ # this cz component).
2620
+ p_cz *= y
2463
2621
  p_i = (1 - p_net_swap) * (1 - p_cz)
2464
2622
  p_x = p_net_swap * (1 - p_cz)
2465
2623
  p_z = (1 - p_net_swap) * p_cz
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu
3
- Version: 2.18.0
3
+ Version: 2.18.2
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.0"
10
+ VERSION = "2.18.2"
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')
File without changes
File without changes
File without changes
File without changes
File without changes