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.
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/MANIFEST.in +0 -1
- {pyqrack_cpu-2.4.0/pyqrack_cpu.egg-info → pyqrack_cpu-2.5.1}/PKG-INFO +1 -1
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_ace_backend.py +131 -235
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1/pyqrack_cpu.egg-info}/PKG-INFO +1 -1
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/SOURCES.txt +0 -1
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/setup.py +1 -1
- pyqrack_cpu-2.4.0/pyqrack/scripts/optimize_bseq_correction.py +0 -443
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/LICENSE +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/Makefile +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/README.md +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyproject.toml +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/__init__.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/neuron_activation_fn.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/pauli.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_circuit.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_neuron.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_neuron_torch_layer.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_simulator.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_stabilizer.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_system/__init__.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/qrack_system/qrack_system.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/quimb_circuit_type.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/stats/__init__.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/stats/load_quantized_data.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack/stats/quantize_by_range.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/dependency_links.txt +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/not-zip-safe +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/requires.txt +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/pyqrack_cpu.egg-info/top_level.txt +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.1}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
#
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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):
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|