pyqrack-cpu 2.4.0__tar.gz → 2.5.1__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 (31) hide show
  1. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/MANIFEST.in +0 -1
  2. {pyqrack_cpu-2.4.0/pyqrack_cpu.egg-info → pyqrack_cpu-2.5.1}/PKG-INFO +1 -1
  3. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_ace_backend.py +131 -235
  4. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1/pyqrack_cpu.egg-info}/PKG-INFO +1 -1
  5. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/SOURCES.txt +0 -1
  6. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/setup.py +1 -1
  7. pyqrack_cpu-2.4.0/pyqrack/scripts/optimize_bseq_correction.py +0 -443
  8. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/LICENSE +0 -0
  9. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/Makefile +0 -0
  10. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/README.md +0 -0
  11. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyproject.toml +0 -0
  12. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/__init__.py +0 -0
  13. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/neuron_activation_fn.py +0 -0
  14. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/pauli.py +0 -0
  15. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_circuit.py +0 -0
  16. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  17. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_neuron.py +0 -0
  18. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  19. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_simulator.py +0 -0
  20. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_stabilizer.py +0 -0
  21. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_system/__init__.py +0 -0
  22. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_system/qrack_system.py +0 -0
  23. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/quimb_circuit_type.py +0 -0
  24. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/stats/__init__.py +0 -0
  25. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/stats/load_quantized_data.py +0 -0
  26. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/stats/quantize_by_range.py +0 -0
  27. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/dependency_links.txt +0 -0
  28. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/not-zip-safe +0 -0
  29. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/requires.txt +0 -0
  30. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/top_level.txt +0 -0
  31. {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/setup.cfg +0 -0
@@ -1,4 +1,3 @@
1
1
  include pyqrack/qrack_system/qrack_lib/*
2
2
  include pyqrack/qrack_system/qrack_cl_precompile/*
3
- include pyqrack/scripts/*
4
3
  include Makefile
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu
3
- Version: 2.4.0
3
+ Version: 2.5.1
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -230,7 +230,6 @@ class QrackAceBackend:
230
230
  is_host_pointer=(True if os.environ.get("PYQRACK_HOST_POINTER_DEFAULT_ON") else False),
231
231
  is_near_clifford_tableau_writer=False,
232
232
  noise=0,
233
- bseq_ancillae=True,
234
233
  to_clone=None,
235
234
  ):
236
235
  if to_clone:
@@ -238,7 +237,6 @@ class QrackAceBackend:
238
237
  long_range_columns = to_clone.long_range_columns
239
238
  long_range_rows = to_clone.long_range_rows
240
239
  is_transpose = to_clone.is_transpose
241
- bseq_ancillae = to_clone.bseq_ancillae
242
240
  if qubit_count < 0:
243
241
  qubit_count = 0
244
242
  if long_range_columns < 0:
@@ -248,7 +246,6 @@ class QrackAceBackend:
248
246
  self.long_range_columns = long_range_columns
249
247
  self.long_range_rows = long_range_rows
250
248
  self.is_transpose = is_transpose
251
- self.bseq_ancillae = bseq_ancillae
252
249
 
253
250
  fppow = 5
254
251
  if "QRACK_FPPOW" in os.environ:
@@ -298,7 +295,6 @@ class QrackAceBackend:
298
295
 
299
296
  self._qubits = []
300
297
  self._lhv = {}
301
- self._ancilla = {}
302
298
  sim_counts = [0] * sim_count
303
299
  sim_id = 0
304
300
  tot_qubits = 0
@@ -315,19 +311,6 @@ class QrackAceBackend:
315
311
  qubit.append((boundary_sim_id, boundary_count))
316
312
  boundary_count += 1
317
313
 
318
- if self.bseq_ancillae:
319
- # Reserve a second boundary-sim qubit as the BSEQ
320
- # correction ancilla, right alongside slot2.
321
- # Starts at |0> and is prepped fresh into |+>
322
- # (via H) transiently around each readout, then
323
- # acts as the CONTROL on a 4-parameter mcu gate
324
- # targeting slot2 -- see
325
- # _apply_bseq_correction/_reverse_bseq_correction.
326
- # Entirely skipped, allocating nothing extra, when
327
- # bseq_ancillae=False.
328
- self._ancilla[tot_qubits] = (boundary_sim_id, boundary_count)
329
- boundary_count += 1
330
-
331
314
  self._lhv[tot_qubits] = LHVQubit(
332
315
  to_clone=(to_clone._lhv[tot_qubits] if to_clone else None)
333
316
  )
@@ -570,7 +553,6 @@ class QrackAceBackend:
570
553
  def _get_bloch_angles(self, hq):
571
554
  sim = self.sim[hq[0]].clone()
572
555
  q = hq[1]
573
- sim.separate([q])
574
556
 
575
557
  # Z axis
576
558
  z = 1 - 2 * sim.prob(q)
@@ -590,7 +572,17 @@ class QrackAceBackend:
590
572
  inclination = math.atan2(math.sqrt(x**2 + y**2), z)
591
573
  azimuth = math.atan2(y, x)
592
574
 
593
- return azimuth, inclination
575
+ # Separability measure, per QUnit::TrySeparate: a genuinely
576
+ # separable (pure, unentangled) single qubit has Bloch vector
577
+ # length exactly 1 ("on-shell" -- on the surface of the Bloch
578
+ # sphere); a qubit reduced from a larger entangled state has
579
+ # length < 1 ("off-shell interior"). one_minus_r = 1 - |r| is
580
+ # near 0 for a genuinely separable qubit, and grows toward 1 the
581
+ # more entangled (mixed, as seen by this one qubit's reduced
582
+ # density matrix) it actually is.
583
+ one_minus_r = 1.0 - math.sqrt(x**2 + y**2 + z**2)
584
+
585
+ return azimuth, inclination, one_minus_r
594
586
 
595
587
  def _rotate_to_bloch(self, hq, delta_azimuth, delta_inclination):
596
588
  sim = self.sim[hq[0]]
@@ -632,159 +624,6 @@ class QrackAceBackend:
632
624
 
633
625
  sim.mtrx([m00, m01, m10, m11])
634
626
 
635
- # Trainable BSEQ correction, in two layers:
636
- #
637
- # 1. A plain, uncontrolled single-qubit U(theta, phi, lambda) gate
638
- # (QrackSimulator.u), applied directly to the boundary qubit
639
- # (slot2). U then U-adjoint, with nothing else happening to slot2
640
- # in between except a probability read, is mathematically forced
641
- # to be the identity on slot2's actual evolution (U_dag @ U = I),
642
- # while the intermediate readout genuinely depends on whatever
643
- # slot2's value was beforehand whenever U doesn't commute with
644
- # the Z basis. This layer needs no ancilla and is always
645
- # available whenever its 3 angles are set.
646
- #
647
- # 2. Optionally (when bseq_ancillae=True at construction, the
648
- # default, and an ancilla was therefore allocated for this
649
- # site), a second layer: the ancilla is prepped fresh into |+>
650
- # (via H), then acts as CONTROL on a 4-parameter mcu gate
651
- # targeting slot2. This layer runs AFTER the u gate in the
652
- # forward ("compute") direction, and is undone FIRST, in the
653
- # opposite order, during reversal ("uncompute"): mcu-adjoint,
654
- # then H, then u-adjoint last.
655
- #
656
- # PyQrack's mcu follows the same convention as Qiskit's U/CU gates.
657
- # Per Qiskit's own documentation, UGate.inverse() gives
658
- # U(th,ph,la)^dagger = U(-th,-la,-ph) (theta negated, phi/lambda
659
- # swapped-and-negated) -- and this was directly, repeatedly
660
- # verified against the real PyQrack simulator (matching to better
661
- # than 1e-6 on the full state vector, across multiple random seeds)
662
- # for the 3-parameter u as well as the 4-parameter mcu (with gamma
663
- # simply negated alongside the same swap). Note that Qiskit's
664
- # separately-documented CUGate.inverse() page states a *different*
665
- # formula (straight negation of all 4 parameters, no swap) -- that
666
- # formula was tested directly against PyQrack's mcu and does NOT
667
- # match; the swap formula is the one actually verified to work here.
668
- #
669
- # All 7 angles (3 for u, 4 for mcu) are trainable; fit them
670
- # empirically against the measured BSEQ S statistic (or the
671
- # simpler Bell-pair objective) via the companion
672
- # optimize_bseq_correction.py script, which sets them directly on
673
- # this class. Default to None: each layer is inactive (a
674
- # guaranteed no-op) until its own angles are explicitly set, and
675
- # the mcu layer additionally requires bseq_ancillae=True at
676
- # construction (and therefore an ancilla actually allocated for
677
- # the site in question).
678
- _BSEQ_THETA = None
679
- _BSEQ_PHI = None
680
- _BSEQ_LAMBDA = None
681
- _BSEQ_MCU_THETA = None
682
- _BSEQ_MCU_PHI = None
683
- _BSEQ_MCU_LAMBDA = None
684
- _BSEQ_MCU_GAMMA = None
685
-
686
- @staticmethod
687
- def _bseq_u_active():
688
- return (
689
- QrackAceBackend._BSEQ_THETA is not None
690
- and QrackAceBackend._BSEQ_PHI is not None
691
- and QrackAceBackend._BSEQ_LAMBDA is not None
692
- )
693
-
694
- @staticmethod
695
- def _bseq_mcu_active():
696
- return (
697
- QrackAceBackend._BSEQ_MCU_THETA is not None
698
- and QrackAceBackend._BSEQ_MCU_PHI is not None
699
- and QrackAceBackend._BSEQ_MCU_LAMBDA is not None
700
- and QrackAceBackend._BSEQ_MCU_GAMMA is not None
701
- )
702
-
703
- @staticmethod
704
- def _bseq_active():
705
- return QrackAceBackend._bseq_u_active() or QrackAceBackend._bseq_mcu_active()
706
-
707
- def _apply_bseq_correction(self, lq):
708
- """Apply the trained BSEQ correction for one readout (the u
709
- layer, then optionally the ancilla+mcu layer), to be reversed
710
- afterward by _reverse_bseq_correction in the opposite order.
711
- Returns a small state dict if anything was applied (so the
712
- caller can pass it straight to the reversal without
713
- re-deriving it), or None if nothing is active or applicable to
714
- this qubit.
715
-
716
- Fires every time this boundary qubit's state is about to be
717
- read out (prob()/_correct()/m()/force_m() all funnel through
718
- here, so this covers every simulator-API readout for the
719
- qubit).
720
- """
721
- u_active = QrackAceBackend._bseq_u_active()
722
- mcu_active = QrackAceBackend._bseq_mcu_active() and lq in self._ancilla
723
- if not (u_active or mcu_active):
724
- return None
725
- hq = self._unpack(lq)
726
- if len(hq) < 3:
727
- return None
728
- slot2_sim, slot2_idx = hq[2]
729
-
730
- if u_active:
731
- self.sim[slot2_sim].u(
732
- slot2_idx,
733
- QrackAceBackend._BSEQ_THETA,
734
- QrackAceBackend._BSEQ_PHI,
735
- QrackAceBackend._BSEQ_LAMBDA,
736
- )
737
-
738
- anc = None
739
- if mcu_active:
740
- anc_sim, anc_idx = self._ancilla[lq]
741
- if anc_sim == slot2_sim:
742
- anc = (anc_sim, anc_idx)
743
- sim = self.sim[anc_sim]
744
- sim.h(anc_idx)
745
- sim.mcu(
746
- [anc_idx],
747
- slot2_idx,
748
- QrackAceBackend._BSEQ_MCU_THETA,
749
- QrackAceBackend._BSEQ_MCU_PHI,
750
- QrackAceBackend._BSEQ_MCU_LAMBDA,
751
- QrackAceBackend._BSEQ_MCU_GAMMA,
752
- )
753
-
754
- return {"slot2": hq[2], "ancilla": anc, "u_active": u_active}
755
-
756
- def _reverse_bseq_correction(self, lq, state):
757
- """Undo exactly what _apply_bseq_correction did, in the
758
- opposite order: mcu-adjoint and H first (if the ancilla layer
759
- ran), then u-adjoint last. Must be called once for every call
760
- to _apply_bseq_correction that returned non-None, after the
761
- caller is done reading whatever probabilities it needed,
762
- passing the same state dict back.
763
- """
764
- slot2_sim, slot2_idx = state["slot2"]
765
- anc = state["ancilla"]
766
-
767
- if anc is not None:
768
- anc_sim, anc_idx = anc
769
- sim = self.sim[anc_sim]
770
- sim.mcu(
771
- [anc_idx],
772
- slot2_idx,
773
- -QrackAceBackend._BSEQ_MCU_THETA,
774
- -QrackAceBackend._BSEQ_MCU_LAMBDA,
775
- -QrackAceBackend._BSEQ_MCU_PHI,
776
- -QrackAceBackend._BSEQ_MCU_GAMMA,
777
- )
778
- sim.h(anc_idx)
779
-
780
- if state["u_active"]:
781
- self.sim[slot2_sim].u(
782
- slot2_idx,
783
- -QrackAceBackend._BSEQ_THETA,
784
- -QrackAceBackend._BSEQ_LAMBDA,
785
- -QrackAceBackend._BSEQ_PHI,
786
- )
787
-
788
627
  def _correct(self, lq, phase=False, skip_rotation=False):
789
628
  hq = self._unpack(lq)
790
629
 
@@ -793,8 +632,6 @@ class QrackAceBackend:
793
632
 
794
633
  qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
795
634
 
796
- bseq_state = self._apply_bseq_correction(lq)
797
-
798
635
  if phase:
799
636
  for q in qb:
800
637
  b = hq[q]
@@ -803,53 +640,84 @@ class QrackAceBackend:
803
640
  self._lhv[lq].h()
804
641
 
805
642
  if len(hq) == 5:
806
- # RMS
807
- p = [
808
- self.sim[hq[0][0]].prob(hq[0][1]),
809
- self.sim[hq[1][0]].prob(hq[1][1]),
810
- self.sim[hq[2][0]].prob(hq[2][1]),
811
- self.sim[hq[3][0]].prob(hq[3][1]),
812
- self.sim[hq[4][0]].prob(hq[4][1]),
813
- ]
814
- # Balancing suggestion from Elara (the custom OpenAI GPT)
815
- prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + 3 * (p[2] ** 2) + p[3] ** 2 + p[4] ** 2) / 7)
816
- qrms = math.sqrt(
817
- (
818
- (1 - p[0]) ** 2
819
- + (1 - p[1]) ** 2
820
- + 3 * ((1 - p[2]) ** 2)
821
- + (1 - p[3]) ** 2
822
- + (1 - p[4]) ** 2
643
+ p0 = self.sim[hq[0][0]].prob(hq[0][1])
644
+ p1 = self.sim[hq[1][0]].prob(hq[1][1])
645
+ p2 = self.sim[hq[2][0]].prob(hq[2][1])
646
+ p3 = self.sim[hq[3][0]].prob(hq[3][1])
647
+ p4 = self.sim[hq[4][0]].prob(hq[4][1])
648
+ lhv = self._lhv.get(lq)
649
+
650
+ # The 4 "end-cap" replicas (home patch + the 3 patch-partner
651
+ # shadow replicas), by analogy with the 3-replica case's
652
+ # slot1/slot2: vote first via their own RMS pool, UNLESS they
653
+ # are in a genuine 2-vs-2 tie, in which case the crossbar
654
+ # replica (hq[2], already weighted specially in the prior
655
+ # flat-pool code) breaks the tie; the LHV is consulted only
656
+ # as a last-resort fallback if the crossbar itself is
657
+ # ambiguous. This is a direct structural analogy to the
658
+ # validated 3-replica cascade, not independently re-derived
659
+ # for this topology -- carried only as far as that cheap
660
+ # analogy supports, per explicit guidance.
661
+ end_caps = [p0, p1, p3, p4]
662
+ high_count = sum(1 for x in end_caps if x >= 0.5)
663
+ end_caps_tied = high_count == 2
664
+
665
+ if not end_caps_tied:
666
+ prms = math.sqrt(sum(x**2 for x in end_caps) / 4)
667
+ qrms = math.sqrt(sum((1 - x) ** 2 for x in end_caps) / 4)
668
+ eff_prob = (prms + (1 - qrms)) / 2
669
+ result = (
670
+ (random.random() < 0.5)
671
+ if abs(eff_prob - 0.5) <= self._epsilon
672
+ else (eff_prob >= 0.5)
823
673
  )
824
- / 7
825
- )
826
- eff_prob = (prms + (1 - qrms)) / 2
827
- result = (
828
- (random.random() < 0.5)
829
- if abs(eff_prob - 0.5) <= self._epsilon
830
- else (eff_prob >= 0.5)
831
- )
832
- syndrome = (
833
- [1 - p[0], 1 - p[1], 1 - p[2], 1 - p[3], 1 - p[4]]
834
- if result
835
- else [p[0], p[1], p[2], p[3], p[4]]
836
- )
674
+ elif abs(p2 - 0.5) > self._epsilon:
675
+ result = p2 >= 0.5
676
+ elif lhv is not None:
677
+ p_lhv = lhv.prob()
678
+ result = (
679
+ (random.random() < 0.5)
680
+ if abs(p_lhv - 0.5) <= self._epsilon
681
+ else (p_lhv >= 0.5)
682
+ )
683
+ else:
684
+ result = random.random() < 0.5
685
+
686
+ p = [p0, p1, p2, p3, p4]
687
+ syndrome = [1 - x for x in p] if result else list(p)
837
688
  for q in range(5):
838
689
  if syndrome[q] > (0.5 + self._epsilon):
839
690
  self.sim[hq[q][0]].x(hq[q][1])
840
691
 
841
692
  if not skip_rotation:
842
- a, i = [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]
843
- a[0], i[0] = self._get_bloch_angles(hq[0])
844
- a[1], i[1] = self._get_bloch_angles(hq[1])
845
- a[2], i[2] = self._get_bloch_angles(hq[2])
846
- a[3], i[3] = self._get_bloch_angles(hq[3])
847
- a[4], i[4] = self._get_bloch_angles(hq[4])
848
-
849
- a_target = sum(a) / 5
850
- i_target = sum(i) / 5
851
- for x in range(5):
852
- self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
693
+ a, i, w = [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]
694
+ a[0], i[0], r0 = self._get_bloch_angles(hq[0])
695
+ a[1], i[1], r1 = self._get_bloch_angles(hq[1])
696
+ a[2], i[2], r2 = self._get_bloch_angles(hq[2])
697
+ a[3], i[3], r3 = self._get_bloch_angles(hq[3])
698
+ a[4], i[4], r4 = self._get_bloch_angles(hq[4])
699
+ w = [1 - r0, 1 - r1, 1 - r2, 1 - r3, 1 - r4]
700
+
701
+ w_total = sum(w)
702
+ if w_total > self._epsilon:
703
+ a_target = sum(wx * ax for wx, ax in zip(w, a)) / w_total
704
+ i_target = sum(wx * ix for wx, ix in zip(w, i)) / w_total
705
+ # hq[0] is excluded from the rotation itself, by the
706
+ # same reasoning as the 3-replica case above: it's
707
+ # allocated identically (unconditionally, first, in
708
+ # the qubit's home-patch simulator) before any
709
+ # boundary/crossbar extension logic runs, so it's the
710
+ # replica most likely to carry real, same-simulator
711
+ # coherent entanglement worth protecting from the
712
+ # decoherence a physical rotation would cost it.
713
+ # Indices 1-4 are all crossbar-extension slots with
714
+ # no comparable real entanglement to lose.
715
+ for x in range(1, 5):
716
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
717
+ # If every replica reads as maximally mixed (w_total ~ 0),
718
+ # there is no well-defined direction to rotate toward at
719
+ # all -- skip the rotation rather than rotate toward an
720
+ # arbitrary/undefined target derived from noise.
853
721
 
854
722
  else:
855
723
  lhv = self._lhv.get(lq)
@@ -891,7 +759,27 @@ class QrackAceBackend:
891
759
  p2 = self.sim[hq[2][0]].prob(hq[2][1])
892
760
  p_lhv = lhv.prob()
893
761
 
894
- if (p1 >= 0.5) == (p2 >= 0.5):
762
+ end_caps_agree = (p1 >= 0.5) == (p2 >= 0.5)
763
+ slot0_disagrees_with_end_caps = (
764
+ end_caps_agree
765
+ and (abs(p0 - 0.5) > self._epsilon)
766
+ and ((p0 >= 0.5) != (p1 >= 0.5))
767
+ )
768
+ if slot0_disagrees_with_end_caps:
769
+ # The end-caps agreeing is not, by itself, reliable
770
+ # corroboration: it can equally well mean both are
771
+ # stale (e.g. an interior qubit they were shadow-
772
+ # coupled to has since been measured for real, and
773
+ # only slot0 -- which shares a simulator with that
774
+ # interior qubit -- has actually been updated to
775
+ # reflect it). When slot0 has a real, non-ambiguous
776
+ # opinion that contradicts the agreeing pair, that
777
+ # contradiction is itself the signal that the pair's
778
+ # agreement is stale, not corroborating -- so trust
779
+ # slot0 directly, overriding the agreement-trusts-
780
+ # itself default below.
781
+ result = p0 >= 0.5
782
+ elif end_caps_agree:
895
783
  prms = math.sqrt((p1**2 + p2**2) / 2)
896
784
  qrms = math.sqrt(((1 - p1) ** 2 + (1 - p2) ** 2) / 2)
897
785
  eff_prob = (prms + (1 - qrms)) / 2
@@ -924,16 +812,32 @@ class QrackAceBackend:
924
812
  # _cpauli_lhv, preserving the property that makes it usable
925
813
  # as a non-collapsing tie-breaker of last resort.
926
814
 
927
- if not skip_rotation:
928
- a, i = [0, 0, 0], [0, 0, 0]
929
- a[0], i[0] = self._get_bloch_angles(hq[0])
930
- a[1], i[1] = self._get_bloch_angles(hq[1])
931
- a[2], i[2] = self._get_bloch_angles(hq[2])
932
-
933
- a_target = sum(a) / 3
934
- i_target = sum(i) / 3
935
- for x in range(3):
936
- self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
815
+ if (not skip_rotation) and (not end_caps_agree):
816
+ a, i, w = [0, 0, 0], [0, 0, 0], [0, 0, 0]
817
+ a[0], i[0], r0 = self._get_bloch_angles(hq[0])
818
+ a[1], i[1], r1 = self._get_bloch_angles(hq[1])
819
+ a[2], i[2], r2 = self._get_bloch_angles(hq[2])
820
+ w = [1 - r0, 1 - r1, 1 - r2]
821
+
822
+ w_total = sum(w)
823
+ if w_total > self._epsilon:
824
+ a_target = sum(wx * ax for wx, ax in zip(w, a)) / w_total
825
+ i_target = sum(wx * ix for wx, ix in zip(w, i)) / w_total
826
+ # slot0 (hq[0]) is excluded from the rotation itself:
827
+ # it commonly shares a real simulator with another
828
+ # logical qubit it's still genuinely, coherently
829
+ # entangled with (e.g. an interior control qubit),
830
+ # and physically rotating it -- even toward a
831
+ # well-intentioned averaged target -- is a real
832
+ # decoherence event on that relationship. slot0's
833
+ # Bloch angles still inform the target above (so
834
+ # slot1/slot2 reconcile toward a value that accounts
835
+ # for what slot0 currently shows), but only slot1 and
836
+ # slot2 (the lossy shadow replicas, which have no
837
+ # comparable real entanglement to lose) are actually
838
+ # rotated.
839
+ for x in range(1, 3):
840
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
937
841
 
938
842
  if phase:
939
843
  for q in qb:
@@ -942,9 +846,6 @@ class QrackAceBackend:
942
846
  if lq in self._lhv:
943
847
  self._lhv[lq].h()
944
848
 
945
- if bseq_state is not None:
946
- self._reverse_bseq_correction(lq, bseq_state)
947
-
948
849
  def apply_magnetic_bias(self, q, b):
949
850
  if b == 0:
950
851
  return
@@ -952,7 +853,7 @@ class QrackAceBackend:
952
853
  for x in q:
953
854
  hq = self._unpack(x)
954
855
  for h in hq:
955
- a, i = self._get_bloch_angles(h)
856
+ a, i, _ = self._get_bloch_angles(h)
956
857
  self._rotate_to_bloch(
957
858
  h,
958
859
  math.atan(math.tan(a) * b) - a,
@@ -1304,8 +1205,6 @@ class QrackAceBackend:
1304
1205
  b = hq[0]
1305
1206
  return self.sim[b[0]].prob(b[1])
1306
1207
 
1307
- bseq_state = self._apply_bseq_correction(lq)
1308
-
1309
1208
  self._correct(lq)
1310
1209
  if len(hq) == 5:
1311
1210
  # RMS
@@ -1356,9 +1255,6 @@ class QrackAceBackend:
1356
1255
  prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
1357
1256
  qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
1358
1257
 
1359
- if bseq_state is not None:
1360
- self._reverse_bseq_correction(lq, bseq_state)
1361
-
1362
1258
  return (prms + (1 - qrms)) / 2
1363
1259
 
1364
1260
  def m(self, lq):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu
3
- Version: 2.4.0
3
+ Version: 2.5.1
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -17,7 +17,6 @@ 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/scripts/optimize_bseq_correction.py
21
20
  pyqrack/stats/__init__.py
22
21
  pyqrack/stats/load_quantized_data.py
23
22
  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.4.0"
10
+ VERSION = "2.5.1"
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')
@@ -1,443 +0,0 @@
1
- # Empirical optimizer for QrackAceBackend's BSEQ (Bell/CHSH) correction
2
- # gate(s).
3
- #
4
- # The correction has two layers:
5
- #
6
- # 1. A plain, uncontrolled U(theta, phi, lambda) gate (QrackSimulator.u)
7
- # applied directly to the boundary qubit. Always available; needs
8
- # no ancilla. U then U-adjoint, with nothing else happening to the
9
- # qubit in between except a probability read, is mathematically
10
- # guaranteed to leave the qubit's actual evolution unaffected
11
- # (U_dag @ U = I), while the intermediate readout genuinely depends
12
- # on the qubit's value beforehand whenever U doesn't commute with
13
- # the Z basis.
14
- #
15
- # 2. Optionally (when ancillae=True, the default, both here and at
16
- # QrackAceBackend construction), a second layer applied AFTER the u
17
- # gate: the boundary qubit's ancilla is prepped fresh into |+>, then
18
- # acts as CONTROL on a 4-parameter mcu(theta,phi,lambda,gamma) gate
19
- # targeting the boundary qubit. Reversed in the opposite order
20
- # (mcu-adjoint, then u-adjoint) when uncomputing.
21
- #
22
- # Both layers ONLY wrap prob()'s own readout computation: _correct()'s
23
- # classical cascade (which permanently decides the boundary qubit's
24
- # committed bit for the rest of circuit evolution) runs completely
25
- # unaffected, exactly as it did before this correction existed. The
26
- # net effect: the correction can only ever change what a given
27
- # prob()/m()/force_m() call reports, and never anything a subsequent
28
- # gate in the circuit will actually see -- i.e. it is idempotent with
29
- # respect to circuit evolution, applying only to observable output.
30
- #
31
- # PyQrack's mcu follows the same convention as Qiskit's U/CU gates. Per
32
- # Qiskit's own documentation, UGate.inverse() gives U(th,ph,la)^dagger
33
- # = U(-th,-la,-ph) (theta negated, phi/lambda swapped-and-negated) --
34
- # directly verified against the real PyQrack simulator (matching to
35
- # better than 1e-6 on the full state vector) for both the 3-parameter u
36
- # and the 4-parameter mcu (gamma simply negated alongside the same
37
- # swap). Note Qiskit's separately-documented CUGate.inverse() page
38
- # states a different formula (straight negation, no swap) -- tested
39
- # directly against PyQrack's mcu, that formula does NOT match; the
40
- # swap formula is the one verified to actually work here.
41
- #
42
- # With ancillae=True (default), all 7 angles (3 for u, 4 for mcu) are
43
- # fit together. With ancillae=False, only the 3 u angles are fit (and
44
- # QrackAceBackend should be constructed with bseq_ancillae=False too,
45
- # so no ancilla qubits are allocated at all). They default to None
46
- # (each layer inactive, a guaranteed no-op) until set here, fit by
47
- # direct, gradient-free search against the empirically measured CHSH S
48
- # statistic (or its 4 components individually), averaged over repeated
49
- # Bell-pair trials to average down the substantial per-construction
50
- # measurement variance inherent to this architecture (each circuit
51
- # construction makes its own one-time classical collapse choice early
52
- # in the elision pipeline; averaging over MANY independent
53
- # constructions is what makes the mean S a meaningful, optimizable
54
- # target -- a single fixed gate cannot reduce per-shot variance, since
55
- # that variance is set by an earlier, already-classical decision the
56
- # gate cannot retroactively undo).
57
- #
58
- # Usage:
59
- # python3 optimize_bseq_correction.py [--iterations N] [--time-budget SECONDS] [--no-ancillae]
60
- #
61
- # Writes the found angles directly into pyqrack's installed
62
- # qrack_ace_backend.py, so every subsequently-constructed
63
- # QrackAceBackend in the running process (and, if you choose to
64
- # persist the file, future processes) uses the fitted correction
65
- # automatically -- no separate model file, no extra inference
66
- # dependency, and no torch/JIT artifact needed: the correction is at
67
- # most 7 floats, and baking them in as class attributes is already the
68
- # most efficient possible packaging.
69
-
70
- import argparse
71
- import math
72
- import random
73
- import statistics
74
- import time
75
-
76
- from pyqrack import QrackAceBackend, Pauli
77
- import pyqrack.qrack_ace_backend as _ace_mod
78
-
79
- PI = math.pi
80
-
81
- # Standard CHSH angle set.
82
- THETA_A, THETA_AP = 0.0, PI / 2
83
- THETA_B, THETA_BP = PI / 4, -PI / 4
84
-
85
-
86
- def _apply_measurement_basis(sim, q, theta):
87
- sim.r(Pauli.PauliY, theta, q)
88
-
89
-
90
- def _measure_expectation(sim, shots):
91
- results = sim.measure_shots([0, 1], shots)
92
- total = 0
93
- for res in results:
94
- a = (res >> 0) & 1
95
- b = (res >> 1) & 1
96
- total += 1 if a == b else -1
97
- return total / shots
98
-
99
-
100
- def _expectation(theta1, theta2, shots, width=16, long_range_columns=1, ancillae=True):
101
- """One fresh Bell-pair construction, measured at a given basis-angle
102
- pair. This is exactly the bseq.py pattern: h(0), cx(0,1), then a
103
- PauliY rotation on each half before measuring."""
104
- s = QrackAceBackend(width, long_range_columns=long_range_columns, bseq_ancillae=ancillae)
105
- s.h(0)
106
- s.mcx([0], 1)
107
- _apply_measurement_basis(s, 0, theta1)
108
- _apply_measurement_basis(s, 1, theta2)
109
- return _measure_expectation(s, shots)
110
-
111
-
112
- def compute_chsh_components(shots, **kwargs):
113
- """Return the 4 CHSH correlator terms (E(a,b), E(a,b'), E(a',b), E(a',b'))."""
114
- return (
115
- _expectation(THETA_A, THETA_B, shots, **kwargs),
116
- _expectation(THETA_A, THETA_BP, shots, **kwargs),
117
- _expectation(THETA_AP, THETA_B, shots, **kwargs),
118
- _expectation(THETA_AP, THETA_BP, shots, **kwargs),
119
- )
120
-
121
-
122
- def compute_S(shots, **kwargs):
123
- e_ab, e_abp, e_apb, e_apbp = compute_chsh_components(shots, **kwargs)
124
- return e_ab + e_abp + e_apb - e_apbp
125
-
126
-
127
- def compute_bell_pair_stats(
128
- n_constructions, shots, width=3, long_range_columns=1, ancillae=True, control=0
129
- ):
130
- """Simpler, plain Bell-pair statistic (matching bell_state.py): for
131
- each of n_constructions independent constructions, put qubit
132
- `control` into superposition and CNOT from it onto the other qubit
133
- (control=0: h(0); cx(0,1), matching bell_state.py exactly;
134
- control=1: h(1); cx(1,0), the mirrored direction -- qubit 1 is the
135
- boundary qubit in this 3-qubit, long_range_columns=1 topology, so
136
- these two directions exercise genuinely different code paths, not
137
- just relabeled qubits). Samples shots measurements and tracks the
138
- 0/1 (agreement-side) balance and overall correlation. Returns
139
- (mean_correlation, mean_balance, stdev_balance).
140
- """
141
- target = 1 - control
142
- total_correlated = 0
143
- balance_vals = []
144
- for _ in range(n_constructions):
145
- s = QrackAceBackend(width, long_range_columns=long_range_columns, bseq_ancillae=ancillae)
146
- s.h(control)
147
- s.cx(control, target)
148
- results = s.measure_shots([0, 1], shots)
149
- zero = sum(1 for r in results if r == 0)
150
- three = sum(1 for r in results if r == 3)
151
- correlated = zero + three
152
- total_correlated += correlated
153
- if correlated:
154
- balance_vals.append(zero / correlated)
155
- mean_corr = total_correlated / (n_constructions * shots)
156
- mean_bal = statistics.mean(balance_vals) if balance_vals else 0.5
157
- stdev_bal = statistics.stdev(balance_vals) if len(balance_vals) > 1 else 0.0
158
- return mean_corr, mean_bal, stdev_bal
159
-
160
-
161
- def bell_pair_score(params, n_constructions=200, shots=24, ancillae=True, **kwargs):
162
- """A single scalar reward for the Bell-pair objective: reward high
163
- correlation, balance centered at 0.5, and low variance in that
164
- balance across constructions. Weighted simply; adjust to taste.
165
- params is a 3-tuple (u angles only) when ancillae=False, or a
166
- 7-tuple (u angles + mcu angles) when ancillae=True."""
167
- set_correction(params, ancillae=ancillae)
168
- mean_corr, mean_bal, stdev_bal = compute_bell_pair_stats(
169
- n_constructions, shots, ancillae=ancillae, **kwargs
170
- )
171
- return mean_corr - abs(mean_bal - 0.5) - 0.5 * stdev_bal
172
-
173
-
174
- def bell_correlation_score(params, n_constructions=200, shots=24, ancillae=True, **kwargs):
175
- """A simpler reward: just the mean Bell-pair correlation itself
176
- (matching bell_state.py's printed "Correlation:" metric exactly --
177
- correlated/shots, averaged over many independent constructions),
178
- with no balance-centering or variance penalty. Note: unlike
179
- bell_pair_score, this has no guard against the optimizer finding a
180
- correction that pushes correlation up by forcing deterministic
181
- agreement between the two qubits rather than by improving genuine
182
- entanglement fidelity -- it is the more direct, but less
183
- constrained, objective."""
184
- set_correction(params, ancillae=ancillae)
185
- mean_corr, _, _ = compute_bell_pair_stats(n_constructions, shots, ancillae=ancillae, **kwargs)
186
- return mean_corr
187
-
188
-
189
- def bell_correlation_symmetric_score(
190
- params, n_constructions=200, shots=24, ancillae=True, **kwargs
191
- ):
192
- """Like bell_correlation_score, but fit simultaneously, with equal
193
- weight, against BOTH CNOT directions: control=0 (h(0); cx(0,1),
194
- the original bell_state.py case) and control=1 (h(1); cx(1,0), the
195
- mirrored case). Returns the unweighted average of the two
196
- directions' mean correlations, so a correction that helps one
197
- direction while hurting the other equally is NOT rewarded --
198
- direct testing showed the correction trained on control=0 alone
199
- actively hurts control=1 (correlation dropping to ~0.17, well
200
- below the ~0.5 chance baseline), confirming the two directions
201
- exercise genuinely different code paths (qubit 1 is the boundary
202
- qubit in this topology) and need to be fit together, not assumed
203
- symmetric."""
204
- set_correction(params, ancillae=ancillae)
205
- mean_corr_0, _, _ = compute_bell_pair_stats(
206
- n_constructions, shots, ancillae=ancillae, control=0, **kwargs
207
- )
208
- mean_corr_1, _, _ = compute_bell_pair_stats(
209
- n_constructions, shots, ancillae=ancillae, control=1, **kwargs
210
- )
211
- return (mean_corr_0 + mean_corr_1) / 2
212
-
213
-
214
- def set_correction(params, ancillae=True):
215
- """params is a 3-tuple (theta, phi, lambda) for the u layer alone
216
- when ancillae=False, or a 7-tuple (theta, phi, lambda, mcu_theta,
217
- mcu_phi, mcu_lambda, mcu_gamma) when ancillae=True."""
218
- theta, phi, lam = params[0], params[1], params[2]
219
- _ace_mod.QrackAceBackend._BSEQ_THETA = theta
220
- _ace_mod.QrackAceBackend._BSEQ_PHI = phi
221
- _ace_mod.QrackAceBackend._BSEQ_LAMBDA = lam
222
- if ancillae:
223
- mcu_theta, mcu_phi, mcu_lam, mcu_gam = params[3], params[4], params[5], params[6]
224
- _ace_mod.QrackAceBackend._BSEQ_MCU_THETA = mcu_theta
225
- _ace_mod.QrackAceBackend._BSEQ_MCU_PHI = mcu_phi
226
- _ace_mod.QrackAceBackend._BSEQ_MCU_LAMBDA = mcu_lam
227
- _ace_mod.QrackAceBackend._BSEQ_MCU_GAMMA = mcu_gam
228
- else:
229
- _ace_mod.QrackAceBackend._BSEQ_MCU_THETA = None
230
- _ace_mod.QrackAceBackend._BSEQ_MCU_PHI = None
231
- _ace_mod.QrackAceBackend._BSEQ_MCU_LAMBDA = None
232
- _ace_mod.QrackAceBackend._BSEQ_MCU_GAMMA = None
233
-
234
-
235
- def mean_reward(params, n_repeats, shots, ancillae=True, **kwargs):
236
- """The actual optimization target: mean S across many independent
237
- Bell-pair constructions ("an RCS data set for Bell-pair
238
- perturbations"), not a single noisy reading."""
239
- set_correction(params, ancillae=ancillae)
240
- vals = [compute_S(shots, ancillae=ancillae, **kwargs) for _ in range(n_repeats)]
241
- return statistics.mean(vals)
242
-
243
-
244
- def optimize(
245
- n_repeats=4,
246
- shots=64,
247
- time_budget=200.0,
248
- max_iterations=None,
249
- seed=None,
250
- initial_step=PI / 4,
251
- step_decay=0.8,
252
- decay_every=10,
253
- target="correlation_symmetric",
254
- ancillae=True,
255
- ):
256
- """Gradient-free random-walk search over the correction angles,
257
- maximizing the chosen reward. Measurement sampling is not
258
- differentiable, so this uses repeated empirical evaluation rather
259
- than backprop -- appropriate given the actual cost here is at most
260
- 7 scalars, not a network.
261
-
262
- ancillae: True (default) fits all 7 parameters (3 for the u layer,
263
- 4 for the ancilla+mcu layer) and constructs QrackAceBackend
264
- with bseq_ancillae=True. False fits only the 3 u-layer
265
- parameters and constructs with bseq_ancillae=False (no ancilla
266
- qubits allocated at all).
267
-
268
- target: "correlation_symmetric" (default) maximizes mean Bell-pair
269
- correlation, fit simultaneously with EQUAL weight against both
270
- CNOT directions (control=0, the original bell_state.py case,
271
- and control=1, the mirrored case h(1); cx(1,0)) -- a correction
272
- that helps one direction while hurting the other is not
273
- rewarded. Direct testing showed these two directions are NOT
274
- symmetric in this architecture (qubit 1 is the boundary qubit
275
- in the 3-qubit, long_range_columns=1 topology): a correction
276
- fit only on control=0 actively harmed control=1 (correlation
277
- dropping to ~0.17, below the ~0.5 chance baseline). "correlation"
278
- optimizes only the original control=0 direction. "bell_pair"
279
- optimizes a composite of correlation, balance-centering, and
280
- low variance (control=0 only). "S" optimizes the CHSH S
281
- statistic directly. In direct testing, "S" did not show a
282
- consistent improvement.
283
- """
284
- if seed is not None:
285
- random.seed(seed)
286
-
287
- if target == "S":
288
- reward_fn = lambda p: mean_reward(p, n_repeats, shots, ancillae=ancillae)
289
- label = "mean_S"
290
- elif target == "bell_pair":
291
- reward_fn = lambda p: bell_pair_score(
292
- p, n_constructions=n_repeats * 32, shots=shots, ancillae=ancillae
293
- )
294
- label = "bell_pair_score"
295
- elif target == "correlation":
296
- reward_fn = lambda p: bell_correlation_score(
297
- p, n_constructions=n_repeats * 32, shots=shots, ancillae=ancillae
298
- )
299
- label = "mean_correlation"
300
- elif target == "correlation_symmetric":
301
- reward_fn = lambda p: bell_correlation_symmetric_score(
302
- p, n_constructions=n_repeats * 32, shots=shots, ancillae=ancillae
303
- )
304
- label = "mean_correlation_symmetric"
305
- else:
306
- raise ValueError(
307
- f"unknown target: {target!r} (expected 'S', 'bell_pair', "
308
- "'correlation', or 'correlation_symmetric')"
309
- )
310
-
311
- n_params = 7 if ancillae else 3
312
- best_params = tuple(0.0 for _ in range(n_params))
313
- best_score = reward_fn(best_params)
314
- print(f"start: {label}={best_score:.4f}")
315
-
316
- t_start = time.time()
317
- step = initial_step
318
- iteration = 0
319
- while True:
320
- if max_iterations is not None and iteration >= max_iterations:
321
- break
322
- if time_budget is not None and (time.time() - t_start) >= time_budget:
323
- break
324
-
325
- candidate = tuple(p + random.uniform(-step, step) for p in best_params)
326
- score = reward_fn(candidate)
327
- if score > best_score:
328
- best_score = score
329
- best_params = candidate
330
- print(
331
- f"iter {iteration}: improved -> "
332
- f"params={[round(p, 4) for p in candidate]} {label}={score:.4f}"
333
- )
334
-
335
- iteration += 1
336
- if iteration % decay_every == 0:
337
- step *= step_decay
338
-
339
- print()
340
- print(f"FINAL params: {[round(p, 6) for p in best_params]}")
341
- print(f"FINAL {label}: {best_score:.4f}")
342
- if target == "S":
343
- print(f" (classical bound: 2.0, Tsirelson: {2*math.sqrt(2):.4f})")
344
- print(f"total iterations: {iteration}")
345
- return best_params, best_score
346
-
347
-
348
- def write_params_to_backend_file(params):
349
- """Patch the installed pyqrack/qrack_ace_backend.py in place, so the
350
- fitted correction persists for future processes too, without
351
- requiring a separate model-loading step at import time. params is
352
- a 3-tuple (u only) or a 7-tuple (u + mcu)."""
353
- import os
354
- import re
355
-
356
- path = os.path.join(os.path.dirname(_ace_mod.__file__), "qrack_ace_backend.py")
357
- with open(path, "r") as f:
358
- src = f.read()
359
-
360
- theta, phi, lam = params[0], params[1], params[2]
361
- src = re.sub(r"_BSEQ_THETA\s*=\s*(None|[-0-9.eE]+)", f"_BSEQ_THETA = {theta!r}", src)
362
- src = re.sub(r"_BSEQ_PHI\s*=\s*(None|[-0-9.eE]+)", f"_BSEQ_PHI = {phi!r}", src)
363
- src = re.sub(r"_BSEQ_LAMBDA\s*=\s*(None|[-0-9.eE]+)", f"_BSEQ_LAMBDA = {lam!r}", src)
364
-
365
- if len(params) >= 7:
366
- mcu_theta, mcu_phi, mcu_lam, mcu_gam = params[3], params[4], params[5], params[6]
367
- src = re.sub(
368
- r"_BSEQ_MCU_THETA\s*=\s*(None|[-0-9.eE]+)",
369
- f"_BSEQ_MCU_THETA = {mcu_theta!r}",
370
- src,
371
- )
372
- src = re.sub(
373
- r"_BSEQ_MCU_PHI\s*=\s*(None|[-0-9.eE]+)", f"_BSEQ_MCU_PHI = {mcu_phi!r}", src
374
- )
375
- src = re.sub(
376
- r"_BSEQ_MCU_LAMBDA\s*=\s*(None|[-0-9.eE]+)",
377
- f"_BSEQ_MCU_LAMBDA = {mcu_lam!r}",
378
- src,
379
- )
380
- src = re.sub(
381
- r"_BSEQ_MCU_GAMMA\s*=\s*(None|[-0-9.eE]+)",
382
- f"_BSEQ_MCU_GAMMA = {mcu_gam!r}",
383
- src,
384
- )
385
-
386
- with open(path, "w") as f:
387
- f.write(src)
388
- print(f"Wrote fitted angles into {path}")
389
-
390
-
391
- def main():
392
- parser = argparse.ArgumentParser(description=__doc__)
393
- parser.add_argument("--iterations", type=int, default=None)
394
- parser.add_argument("--time-budget", type=float, default=200.0)
395
- parser.add_argument("--repeats", type=int, default=4)
396
- parser.add_argument("--shots", type=int, default=64)
397
- parser.add_argument("--seed", type=int, default=None)
398
- parser.add_argument(
399
- "--target",
400
- choices=["S", "bell_pair", "correlation", "correlation_symmetric"],
401
- default="correlation_symmetric",
402
- help="Optimization objective: 'correlation_symmetric' (default) "
403
- "maximizes mean Bell-pair correlation, fit with equal weight "
404
- "against both CNOT directions (control=0 and control=1, which "
405
- "are NOT symmetric in this architecture); 'correlation' fits "
406
- "only the original control=0 direction; 'bell_pair' optimizes "
407
- "a composite of correlation, balance-centering, and low "
408
- "variance (control=0 only); 'S' optimizes the CHSH S "
409
- "statistic directly.",
410
- )
411
- parser.add_argument(
412
- "--no-ancillae",
413
- dest="ancillae",
414
- action="store_false",
415
- default=True,
416
- help="Fit only the 3-parameter u layer (no ancilla qubits "
417
- "allocated at all, QrackAceBackend constructed with "
418
- "bseq_ancillae=False). Default: fit all 7 parameters (u layer "
419
- "+ ancilla/mcu layer), with bseq_ancillae=True.",
420
- )
421
- parser.add_argument(
422
- "--persist",
423
- action="store_true",
424
- help="Write the fitted angles into the installed backend file.",
425
- )
426
- args = parser.parse_args()
427
-
428
- params, score = optimize(
429
- n_repeats=args.repeats,
430
- shots=args.shots,
431
- time_budget=args.time_budget,
432
- max_iterations=args.iterations,
433
- seed=args.seed,
434
- target=args.target,
435
- ancillae=args.ancillae,
436
- )
437
-
438
- if args.persist:
439
- write_params_to_backend_file(params)
440
-
441
-
442
- if __name__ == "__main__":
443
- main()
File without changes
File without changes
File without changes
File without changes
File without changes