pyqrack-cpu 2.4.0__tar.gz → 2.5.0__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.0}/MANIFEST.in +0 -1
- {pyqrack_cpu-2.4.0/pyqrack_cpu.egg-info → pyqrack_cpu-2.5.0}/PKG-INFO +1 -1
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_ace_backend.py +63 -204
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0/pyqrack_cpu.egg-info}/PKG-INFO +1 -1
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack_cpu.egg-info/SOURCES.txt +0 -1
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/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.0}/LICENSE +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/Makefile +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/README.md +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyproject.toml +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/__init__.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/neuron_activation_fn.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/pauli.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_circuit.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_neuron.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_neuron_torch_layer.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_simulator.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_stabilizer.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_system/__init__.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/qrack_system/qrack_system.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/quimb_circuit_type.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/stats/__init__.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/stats/load_quantized_data.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack/stats/quantize_by_range.py +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack_cpu.egg-info/dependency_links.txt +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack_cpu.egg-info/not-zip-safe +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack_cpu.egg-info/requires.txt +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/pyqrack_cpu.egg-info/top_level.txt +0 -0
- {pyqrack_cpu-2.4.0 → pyqrack_cpu-2.5.0}/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]
|
|
@@ -839,17 +676,24 @@ class QrackAceBackend:
|
|
|
839
676
|
self.sim[hq[q][0]].x(hq[q][1])
|
|
840
677
|
|
|
841
678
|
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
|
-
|
|
679
|
+
a, i, w = [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]
|
|
680
|
+
a[0], i[0], r0 = self._get_bloch_angles(hq[0])
|
|
681
|
+
a[1], i[1], r1 = self._get_bloch_angles(hq[1])
|
|
682
|
+
a[2], i[2], r2 = self._get_bloch_angles(hq[2])
|
|
683
|
+
a[3], i[3], r3 = self._get_bloch_angles(hq[3])
|
|
684
|
+
a[4], i[4], r4 = self._get_bloch_angles(hq[4])
|
|
685
|
+
w = [1 - r0, 1 - r1, 1 - r2, 1 - r3, 1 - r4]
|
|
686
|
+
|
|
687
|
+
w_total = sum(w)
|
|
688
|
+
if w_total > self._epsilon:
|
|
689
|
+
a_target = sum(wx * ax for wx, ax in zip(w, a)) / w_total
|
|
690
|
+
i_target = sum(wx * ix for wx, ix in zip(w, i)) / w_total
|
|
691
|
+
for x in range(5):
|
|
692
|
+
self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
|
|
693
|
+
# If every replica reads as maximally mixed (w_total ~ 0),
|
|
694
|
+
# there is no well-defined direction to rotate toward at
|
|
695
|
+
# all -- skip the rotation rather than rotate toward an
|
|
696
|
+
# arbitrary/undefined target derived from noise.
|
|
853
697
|
|
|
854
698
|
else:
|
|
855
699
|
lhv = self._lhv.get(lq)
|
|
@@ -891,7 +735,27 @@ class QrackAceBackend:
|
|
|
891
735
|
p2 = self.sim[hq[2][0]].prob(hq[2][1])
|
|
892
736
|
p_lhv = lhv.prob()
|
|
893
737
|
|
|
894
|
-
|
|
738
|
+
end_caps_agree = (p1 >= 0.5) == (p2 >= 0.5)
|
|
739
|
+
slot0_disagrees_with_end_caps = (
|
|
740
|
+
end_caps_agree
|
|
741
|
+
and (abs(p0 - 0.5) > self._epsilon)
|
|
742
|
+
and ((p0 >= 0.5) != (p1 >= 0.5))
|
|
743
|
+
)
|
|
744
|
+
if slot0_disagrees_with_end_caps:
|
|
745
|
+
# The end-caps agreeing is not, by itself, reliable
|
|
746
|
+
# corroboration: it can equally well mean both are
|
|
747
|
+
# stale (e.g. an interior qubit they were shadow-
|
|
748
|
+
# coupled to has since been measured for real, and
|
|
749
|
+
# only slot0 -- which shares a simulator with that
|
|
750
|
+
# interior qubit -- has actually been updated to
|
|
751
|
+
# reflect it). When slot0 has a real, non-ambiguous
|
|
752
|
+
# opinion that contradicts the agreeing pair, that
|
|
753
|
+
# contradiction is itself the signal that the pair's
|
|
754
|
+
# agreement is stale, not corroborating -- so trust
|
|
755
|
+
# slot0 directly, overriding the agreement-trusts-
|
|
756
|
+
# itself default below.
|
|
757
|
+
result = p0 >= 0.5
|
|
758
|
+
elif end_caps_agree:
|
|
895
759
|
prms = math.sqrt((p1**2 + p2**2) / 2)
|
|
896
760
|
qrms = math.sqrt(((1 - p1) ** 2 + (1 - p2) ** 2) / 2)
|
|
897
761
|
eff_prob = (prms + (1 - qrms)) / 2
|
|
@@ -924,16 +788,19 @@ class QrackAceBackend:
|
|
|
924
788
|
# _cpauli_lhv, preserving the property that makes it usable
|
|
925
789
|
# as a non-collapsing tie-breaker of last resort.
|
|
926
790
|
|
|
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])
|
|
791
|
+
if (not skip_rotation) and (not end_caps_agree):
|
|
792
|
+
a, i, w = [0, 0, 0], [0, 0, 0], [0, 0, 0]
|
|
793
|
+
a[0], i[0], r0 = self._get_bloch_angles(hq[0])
|
|
794
|
+
a[1], i[1], r1 = self._get_bloch_angles(hq[1])
|
|
795
|
+
a[2], i[2], r2 = self._get_bloch_angles(hq[2])
|
|
796
|
+
w = [1 - r0, 1 - r1, 1 - r2]
|
|
932
797
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
798
|
+
w_total = sum(w)
|
|
799
|
+
if w_total > self._epsilon:
|
|
800
|
+
a_target = sum(wx * ax for wx, ax in zip(w, a)) / w_total
|
|
801
|
+
i_target = sum(wx * ix for wx, ix in zip(w, i)) / w_total
|
|
802
|
+
for x in range(3):
|
|
803
|
+
self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
|
|
937
804
|
|
|
938
805
|
if phase:
|
|
939
806
|
for q in qb:
|
|
@@ -942,9 +809,6 @@ class QrackAceBackend:
|
|
|
942
809
|
if lq in self._lhv:
|
|
943
810
|
self._lhv[lq].h()
|
|
944
811
|
|
|
945
|
-
if bseq_state is not None:
|
|
946
|
-
self._reverse_bseq_correction(lq, bseq_state)
|
|
947
|
-
|
|
948
812
|
def apply_magnetic_bias(self, q, b):
|
|
949
813
|
if b == 0:
|
|
950
814
|
return
|
|
@@ -952,7 +816,7 @@ class QrackAceBackend:
|
|
|
952
816
|
for x in q:
|
|
953
817
|
hq = self._unpack(x)
|
|
954
818
|
for h in hq:
|
|
955
|
-
a, i = self._get_bloch_angles(h)
|
|
819
|
+
a, i, _ = self._get_bloch_angles(h)
|
|
956
820
|
self._rotate_to_bloch(
|
|
957
821
|
h,
|
|
958
822
|
math.atan(math.tan(a) * b) - a,
|
|
@@ -1304,8 +1168,6 @@ class QrackAceBackend:
|
|
|
1304
1168
|
b = hq[0]
|
|
1305
1169
|
return self.sim[b[0]].prob(b[1])
|
|
1306
1170
|
|
|
1307
|
-
bseq_state = self._apply_bseq_correction(lq)
|
|
1308
|
-
|
|
1309
1171
|
self._correct(lq)
|
|
1310
1172
|
if len(hq) == 5:
|
|
1311
1173
|
# RMS
|
|
@@ -1356,9 +1218,6 @@ class QrackAceBackend:
|
|
|
1356
1218
|
prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
|
|
1357
1219
|
qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
|
|
1358
1220
|
|
|
1359
|
-
if bseq_state is not None:
|
|
1360
|
-
self._reverse_bseq_correction(lq, bseq_state)
|
|
1361
|
-
|
|
1362
1221
|
return (prms + (1 - qrms)) / 2
|
|
1363
1222
|
|
|
1364
1223
|
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
|