pyqrack-cpu 2.3.0__tar.gz → 2.3.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.3.0/pyqrack_cpu.egg-info → pyqrack_cpu-2.3.1}/PKG-INFO +1 -1
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_ace_backend.py +172 -118
- pyqrack_cpu-2.3.1/pyqrack/scripts/optimize_bseq_correction.py +370 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1/pyqrack_cpu.egg-info}/PKG-INFO +1 -1
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/setup.py +1 -1
- pyqrack_cpu-2.3.0/pyqrack/scripts/optimize_bseq_correction.py +0 -207
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/LICENSE +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/MANIFEST.in +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/Makefile +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/README.md +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyproject.toml +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/__init__.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/neuron_activation_fn.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/pauli.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_circuit.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_neuron.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_neuron_torch_layer.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_simulator.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_stabilizer.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_system/__init__.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/qrack_system/qrack_system.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/quimb_circuit_type.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/stats/__init__.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/stats/load_quantized_data.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack/stats/quantize_by_range.py +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack_cpu.egg-info/SOURCES.txt +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack_cpu.egg-info/dependency_links.txt +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack_cpu.egg-info/not-zip-safe +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack_cpu.egg-info/requires.txt +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/pyqrack_cpu.egg-info/top_level.txt +0 -0
- {pyqrack_cpu-2.3.0 → pyqrack_cpu-2.3.1}/setup.cfg +0 -0
|
@@ -28,39 +28,6 @@ try:
|
|
|
28
28
|
except ImportError:
|
|
29
29
|
_IS_QISKIT_AER_AVAILABLE = False
|
|
30
30
|
|
|
31
|
-
_IS_TORCH_AVAILABLE = True
|
|
32
|
-
try:
|
|
33
|
-
import torch
|
|
34
|
-
import torch.nn as nn
|
|
35
|
-
except ImportError:
|
|
36
|
-
_IS_TORCH_AVAILABLE = False
|
|
37
|
-
|
|
38
|
-
_BSEQ_CORRECTOR_MODEL = None
|
|
39
|
-
_BSEQ_CORRECTOR_LOAD_ATTEMPTED = False
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _get_bseq_corrector():
|
|
43
|
-
"""Lazily load the trained BSEQ (Bell/CHSH) boundary corrector model.
|
|
44
|
-
|
|
45
|
-
Returns None if torch isn't available or the model file isn't found,
|
|
46
|
-
in which case callers fall back to the existing non-learned cascade.
|
|
47
|
-
Memoized at module scope: loaded at most once per process.
|
|
48
|
-
"""
|
|
49
|
-
global _BSEQ_CORRECTOR_MODEL, _BSEQ_CORRECTOR_LOAD_ATTEMPTED
|
|
50
|
-
if _BSEQ_CORRECTOR_LOAD_ATTEMPTED:
|
|
51
|
-
return _BSEQ_CORRECTOR_MODEL
|
|
52
|
-
_BSEQ_CORRECTOR_LOAD_ATTEMPTED = True
|
|
53
|
-
if not _IS_TORCH_AVAILABLE:
|
|
54
|
-
return None
|
|
55
|
-
model_path = os.path.join(os.path.dirname(__file__), "bseq_corrector.jit.pt")
|
|
56
|
-
try:
|
|
57
|
-
model = torch.jit.load(model_path)
|
|
58
|
-
model.eval()
|
|
59
|
-
_BSEQ_CORRECTOR_MODEL = model
|
|
60
|
-
except Exception:
|
|
61
|
-
_BSEQ_CORRECTOR_MODEL = None
|
|
62
|
-
return _BSEQ_CORRECTOR_MODEL
|
|
63
|
-
|
|
64
31
|
|
|
65
32
|
# Initial stub and concept produced through conversation with Elara
|
|
66
33
|
# (the custom OpenAI GPT)
|
|
@@ -263,6 +230,7 @@ class QrackAceBackend:
|
|
|
263
230
|
is_host_pointer=(True if os.environ.get("PYQRACK_HOST_POINTER_DEFAULT_ON") else False),
|
|
264
231
|
is_near_clifford_tableau_writer=False,
|
|
265
232
|
noise=0,
|
|
233
|
+
bseq_ancillae=True,
|
|
266
234
|
to_clone=None,
|
|
267
235
|
):
|
|
268
236
|
if to_clone:
|
|
@@ -270,6 +238,7 @@ class QrackAceBackend:
|
|
|
270
238
|
long_range_columns = to_clone.long_range_columns
|
|
271
239
|
long_range_rows = to_clone.long_range_rows
|
|
272
240
|
is_transpose = to_clone.is_transpose
|
|
241
|
+
bseq_ancillae = to_clone.bseq_ancillae
|
|
273
242
|
if qubit_count < 0:
|
|
274
243
|
qubit_count = 0
|
|
275
244
|
if long_range_columns < 0:
|
|
@@ -279,6 +248,7 @@ class QrackAceBackend:
|
|
|
279
248
|
self.long_range_columns = long_range_columns
|
|
280
249
|
self.long_range_rows = long_range_rows
|
|
281
250
|
self.is_transpose = is_transpose
|
|
251
|
+
self.bseq_ancillae = bseq_ancillae
|
|
282
252
|
|
|
283
253
|
fppow = 5
|
|
284
254
|
if "QRACK_FPPOW" in os.environ:
|
|
@@ -329,9 +299,6 @@ class QrackAceBackend:
|
|
|
329
299
|
self._qubits = []
|
|
330
300
|
self._lhv = {}
|
|
331
301
|
self._ancilla = {}
|
|
332
|
-
self._ancilla_paired = set(to_clone._ancilla_paired) if to_clone else set()
|
|
333
|
-
self._bell_partner = dict(to_clone._bell_partner) if to_clone else {}
|
|
334
|
-
self._last_y_rotation = dict(to_clone._last_y_rotation) if to_clone else {}
|
|
335
302
|
sim_counts = [0] * sim_count
|
|
336
303
|
sim_id = 0
|
|
337
304
|
tot_qubits = 0
|
|
@@ -348,15 +315,18 @@ class QrackAceBackend:
|
|
|
348
315
|
qubit.append((boundary_sim_id, boundary_count))
|
|
349
316
|
boundary_count += 1
|
|
350
317
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
360
330
|
|
|
361
331
|
self._lhv[tot_qubits] = LHVQubit(
|
|
362
332
|
to_clone=(to_clone._lhv[tot_qubits] if to_clone else None)
|
|
@@ -409,7 +379,7 @@ class QrackAceBackend:
|
|
|
409
379
|
)
|
|
410
380
|
)
|
|
411
381
|
|
|
412
|
-
# You can still "
|
|
382
|
+
# You can still "monkey-patch" this, after the constructor.
|
|
413
383
|
# if "QRACK_QUNIT_SEPARABILITY_THRESHOLD" not in os.environ:
|
|
414
384
|
# # (1 - 1 / sqrt(2)) / 4 (but empirically tuned)
|
|
415
385
|
# self.sim[i].set_sdrp(0.073223304703363119)
|
|
@@ -419,10 +389,6 @@ class QrackAceBackend:
|
|
|
419
389
|
def clone(self):
|
|
420
390
|
return QrackAceBackend(to_clone=self)
|
|
421
391
|
|
|
422
|
-
def set_sdrp(self, sdrp):
|
|
423
|
-
for sim in self.sim:
|
|
424
|
-
sim.set_sdrp(sdrp)
|
|
425
|
-
|
|
426
392
|
def measure_shots_consensus(self, q, s, n_instances=3, threshold=0.1):
|
|
427
393
|
# Consensus measurement across n_instances independent clones.
|
|
428
394
|
#
|
|
@@ -666,46 +632,158 @@ class QrackAceBackend:
|
|
|
666
632
|
|
|
667
633
|
sim.mtrx([m00, m01, m10, m11])
|
|
668
634
|
|
|
669
|
-
# Trainable BSEQ correction
|
|
670
|
-
#
|
|
671
|
-
#
|
|
672
|
-
#
|
|
673
|
-
#
|
|
674
|
-
#
|
|
675
|
-
#
|
|
676
|
-
#
|
|
677
|
-
#
|
|
678
|
-
#
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
|
682
685
|
|
|
683
|
-
|
|
684
|
-
|
|
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
|
+
)
|
|
685
702
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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).
|
|
691
720
|
"""
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
695
725
|
hq = self._unpack(lq)
|
|
696
726
|
if len(hq) < 3:
|
|
697
|
-
return
|
|
698
|
-
anc_sim, anc_idx = anc
|
|
727
|
+
return None
|
|
699
728
|
slot2_sim, slot2_idx = hq[2]
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
+
)
|
|
709
787
|
|
|
710
788
|
def _correct(self, lq, phase=False, skip_rotation=False):
|
|
711
789
|
hq = self._unpack(lq)
|
|
@@ -715,6 +793,8 @@ class QrackAceBackend:
|
|
|
715
793
|
|
|
716
794
|
qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
|
|
717
795
|
|
|
796
|
+
bseq_state = self._apply_bseq_correction(lq)
|
|
797
|
+
|
|
718
798
|
if phase:
|
|
719
799
|
for q in qb:
|
|
720
800
|
b = hq[q]
|
|
@@ -806,16 +886,6 @@ class QrackAceBackend:
|
|
|
806
886
|
# weight: trust slot1/slot2 alone whenever they agree, and
|
|
807
887
|
# only consult slot0, then lhv, when they genuinely don't.
|
|
808
888
|
#
|
|
809
|
-
# The BSEQ correction (a real, coherent controlled-U gate
|
|
810
|
-
# on slot2, conditioned on its ancilla) is applied FIRST,
|
|
811
|
-
# directly to the quantum state -- before any probability
|
|
812
|
-
# is read. This composes correctly even while this qubit's
|
|
813
|
-
# Bell-pair partner is still genuinely superposed, unlike
|
|
814
|
-
# every classical-decision approach that was tried and
|
|
815
|
-
# rejected here (all of them were provably forced into an
|
|
816
|
-
# uninformative coin flip in exactly that case).
|
|
817
|
-
self._apply_bseq_correction(lq)
|
|
818
|
-
|
|
819
889
|
p0 = self.sim[hq[0][0]].prob(hq[0][1])
|
|
820
890
|
p1 = self.sim[hq[1][0]].prob(hq[1][1])
|
|
821
891
|
p2 = self.sim[hq[2][0]].prob(hq[2][1])
|
|
@@ -872,6 +942,9 @@ class QrackAceBackend:
|
|
|
872
942
|
if lq in self._lhv:
|
|
873
943
|
self._lhv[lq].h()
|
|
874
944
|
|
|
945
|
+
if bseq_state is not None:
|
|
946
|
+
self._reverse_bseq_correction(lq, bseq_state)
|
|
947
|
+
|
|
875
948
|
def apply_magnetic_bias(self, q, b):
|
|
876
949
|
if b == 0:
|
|
877
950
|
return
|
|
@@ -906,9 +979,6 @@ class QrackAceBackend:
|
|
|
906
979
|
# Correction deferred to next 2-qubit gate (_cpauli calls _correct)
|
|
907
980
|
|
|
908
981
|
def r(self, p, th, lq):
|
|
909
|
-
if p == Pauli.PauliY:
|
|
910
|
-
self._last_y_rotation[lq] = self._last_y_rotation.get(lq, 0.0) + th
|
|
911
|
-
|
|
912
982
|
hq = self._unpack(lq)
|
|
913
983
|
if len(hq) < 2:
|
|
914
984
|
b = hq[0]
|
|
@@ -1141,27 +1211,6 @@ class QrackAceBackend:
|
|
|
1141
1211
|
# qubits, which now live as real qubits in the shared boundary sim.
|
|
1142
1212
|
self._apply_coupling(pauli, anti, qb1, hq1, qb2, hq2, lq1_lr)
|
|
1143
1213
|
|
|
1144
|
-
if lq2 in self._ancilla and lq2 not in self._ancilla_paired and len(hq2) >= 3:
|
|
1145
|
-
anc_sim, anc_idx = self._ancilla[lq2]
|
|
1146
|
-
slot2_sim, slot2_idx = hq2[2]
|
|
1147
|
-
if anc_sim == slot2_sim:
|
|
1148
|
-
# slot2 as CONTROL, ancilla as target: a CX never modifies
|
|
1149
|
-
# its control qubit's own marginal state, so this taps
|
|
1150
|
-
# slot2's real, evolving state non-invasively -- the
|
|
1151
|
-
# ancilla becomes perfectly correlated with slot2 without
|
|
1152
|
-
# disturbing it (unlike the reverse direction, which would
|
|
1153
|
-
# mix/dephase slot2 itself every time pairing occurred).
|
|
1154
|
-
self.sim[anc_sim].mcx([slot2_idx], anc_idx)
|
|
1155
|
-
else:
|
|
1156
|
-
# Different simulators (shouldn't normally happen, since
|
|
1157
|
-
# ancilla and slot2 are allocated in the same boundary sim,
|
|
1158
|
-
# but guard defensively): fall back to a direct two-qubit
|
|
1159
|
-
# gate is unavailable cross-sim here, so this path is not
|
|
1160
|
-
# expected to be exercised in practice.
|
|
1161
|
-
pass
|
|
1162
|
-
self._ancilla_paired.add(lq2)
|
|
1163
|
-
self._bell_partner[lq2] = lq1
|
|
1164
|
-
|
|
1165
1214
|
if lq2 in self._lhv:
|
|
1166
1215
|
ctrl_prob = self.sim[hq1[0][0]].prob(hq1[0][1])
|
|
1167
1216
|
_cpauli_lhv(ctrl_prob, self._lhv[lq2], pauli, anti)
|
|
@@ -1255,6 +1304,8 @@ class QrackAceBackend:
|
|
|
1255
1304
|
b = hq[0]
|
|
1256
1305
|
return self.sim[b[0]].prob(b[1])
|
|
1257
1306
|
|
|
1307
|
+
bseq_state = self._apply_bseq_correction(lq)
|
|
1308
|
+
|
|
1258
1309
|
self._correct(lq)
|
|
1259
1310
|
if len(hq) == 5:
|
|
1260
1311
|
# RMS
|
|
@@ -1305,6 +1356,9 @@ class QrackAceBackend:
|
|
|
1305
1356
|
prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
|
|
1306
1357
|
qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
|
|
1307
1358
|
|
|
1359
|
+
if bseq_state is not None:
|
|
1360
|
+
self._reverse_bseq_correction(lq, bseq_state)
|
|
1361
|
+
|
|
1308
1362
|
return (prms + (1 - qrms)) / 2
|
|
1309
1363
|
|
|
1310
1364
|
def m(self, lq):
|
|
@@ -0,0 +1,370 @@
|
|
|
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(n_constructions, shots, width=3, long_range_columns=1, ancillae=True):
|
|
128
|
+
"""Simpler, plain Bell-pair statistic (matching bell_state.py): for
|
|
129
|
+
each of n_constructions independent h(0); cx(0,1) builds, sample
|
|
130
|
+
shots measurements and track the 0/1 (agreement-side) balance and
|
|
131
|
+
overall correlation. Returns (mean_correlation, mean_balance,
|
|
132
|
+
stdev_balance). In direct testing this correction mechanism affects
|
|
133
|
+
these more reliably than CHSH S -- see module docstring.
|
|
134
|
+
"""
|
|
135
|
+
total_correlated = 0
|
|
136
|
+
balance_vals = []
|
|
137
|
+
for _ in range(n_constructions):
|
|
138
|
+
s = QrackAceBackend(width, long_range_columns=long_range_columns, bseq_ancillae=ancillae)
|
|
139
|
+
s.h(0)
|
|
140
|
+
s.cx(0, 1)
|
|
141
|
+
results = s.measure_shots([0, 1], shots)
|
|
142
|
+
zero = sum(1 for r in results if r == 0)
|
|
143
|
+
three = sum(1 for r in results if r == 3)
|
|
144
|
+
correlated = zero + three
|
|
145
|
+
total_correlated += correlated
|
|
146
|
+
if correlated:
|
|
147
|
+
balance_vals.append(zero / correlated)
|
|
148
|
+
mean_corr = total_correlated / (n_constructions * shots)
|
|
149
|
+
mean_bal = statistics.mean(balance_vals) if balance_vals else 0.5
|
|
150
|
+
stdev_bal = statistics.stdev(balance_vals) if len(balance_vals) > 1 else 0.0
|
|
151
|
+
return mean_corr, mean_bal, stdev_bal
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def bell_pair_score(params, n_constructions=200, shots=24, ancillae=True, **kwargs):
|
|
155
|
+
"""A single scalar reward for the Bell-pair objective: reward high
|
|
156
|
+
correlation, balance centered at 0.5, and low variance in that
|
|
157
|
+
balance across constructions. Weighted simply; adjust to taste.
|
|
158
|
+
params is a 3-tuple (u angles only) when ancillae=False, or a
|
|
159
|
+
7-tuple (u angles + mcu angles) when ancillae=True."""
|
|
160
|
+
set_correction(params, ancillae=ancillae)
|
|
161
|
+
mean_corr, mean_bal, stdev_bal = compute_bell_pair_stats(
|
|
162
|
+
n_constructions, shots, ancillae=ancillae, **kwargs
|
|
163
|
+
)
|
|
164
|
+
return mean_corr - abs(mean_bal - 0.5) - 0.5 * stdev_bal
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def set_correction(params, ancillae=True):
|
|
168
|
+
"""params is a 3-tuple (theta, phi, lambda) for the u layer alone
|
|
169
|
+
when ancillae=False, or a 7-tuple (theta, phi, lambda, mcu_theta,
|
|
170
|
+
mcu_phi, mcu_lambda, mcu_gamma) when ancillae=True."""
|
|
171
|
+
theta, phi, lam = params[0], params[1], params[2]
|
|
172
|
+
_ace_mod.QrackAceBackend._BSEQ_THETA = theta
|
|
173
|
+
_ace_mod.QrackAceBackend._BSEQ_PHI = phi
|
|
174
|
+
_ace_mod.QrackAceBackend._BSEQ_LAMBDA = lam
|
|
175
|
+
if ancillae:
|
|
176
|
+
mcu_theta, mcu_phi, mcu_lam, mcu_gam = params[3], params[4], params[5], params[6]
|
|
177
|
+
_ace_mod.QrackAceBackend._BSEQ_MCU_THETA = mcu_theta
|
|
178
|
+
_ace_mod.QrackAceBackend._BSEQ_MCU_PHI = mcu_phi
|
|
179
|
+
_ace_mod.QrackAceBackend._BSEQ_MCU_LAMBDA = mcu_lam
|
|
180
|
+
_ace_mod.QrackAceBackend._BSEQ_MCU_GAMMA = mcu_gam
|
|
181
|
+
else:
|
|
182
|
+
_ace_mod.QrackAceBackend._BSEQ_MCU_THETA = None
|
|
183
|
+
_ace_mod.QrackAceBackend._BSEQ_MCU_PHI = None
|
|
184
|
+
_ace_mod.QrackAceBackend._BSEQ_MCU_LAMBDA = None
|
|
185
|
+
_ace_mod.QrackAceBackend._BSEQ_MCU_GAMMA = None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def mean_reward(params, n_repeats, shots, ancillae=True, **kwargs):
|
|
189
|
+
"""The actual optimization target: mean S across many independent
|
|
190
|
+
Bell-pair constructions ("an RCS data set for Bell-pair
|
|
191
|
+
perturbations"), not a single noisy reading."""
|
|
192
|
+
set_correction(params, ancillae=ancillae)
|
|
193
|
+
vals = [compute_S(shots, ancillae=ancillae, **kwargs) for _ in range(n_repeats)]
|
|
194
|
+
return statistics.mean(vals)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def optimize(
|
|
198
|
+
n_repeats=4,
|
|
199
|
+
shots=64,
|
|
200
|
+
time_budget=200.0,
|
|
201
|
+
max_iterations=None,
|
|
202
|
+
seed=None,
|
|
203
|
+
initial_step=PI / 4,
|
|
204
|
+
step_decay=0.8,
|
|
205
|
+
decay_every=10,
|
|
206
|
+
target="bell_pair",
|
|
207
|
+
ancillae=True,
|
|
208
|
+
):
|
|
209
|
+
"""Gradient-free random-walk search over the correction angles,
|
|
210
|
+
maximizing the chosen reward. Measurement sampling is not
|
|
211
|
+
differentiable, so this uses repeated empirical evaluation rather
|
|
212
|
+
than backprop -- appropriate given the actual cost here is at most
|
|
213
|
+
7 scalars, not a network.
|
|
214
|
+
|
|
215
|
+
ancillae: True (default) fits all 7 parameters (3 for the u layer,
|
|
216
|
+
4 for the ancilla+mcu layer) and constructs QrackAceBackend
|
|
217
|
+
with bseq_ancillae=True. False fits only the 3 u-layer
|
|
218
|
+
parameters and constructs with bseq_ancillae=False (no ancilla
|
|
219
|
+
qubits allocated at all).
|
|
220
|
+
|
|
221
|
+
target: "S" optimizes the CHSH S statistic directly (the originally
|
|
222
|
+
requested objective); "bell_pair" optimizes the simpler
|
|
223
|
+
correlation/balance/variance objective from bell_state.py. In
|
|
224
|
+
direct testing, "bell_pair" shows a real, reproducible variance
|
|
225
|
+
reduction under this correction mechanism, while "S" did not
|
|
226
|
+
show a consistent improvement -- so "bell_pair" is the default.
|
|
227
|
+
"""
|
|
228
|
+
if seed is not None:
|
|
229
|
+
random.seed(seed)
|
|
230
|
+
|
|
231
|
+
if target == "S":
|
|
232
|
+
reward_fn = lambda p: mean_reward(p, n_repeats, shots, ancillae=ancillae)
|
|
233
|
+
label = "mean_S"
|
|
234
|
+
elif target == "bell_pair":
|
|
235
|
+
reward_fn = lambda p: bell_pair_score(
|
|
236
|
+
p, n_constructions=n_repeats * 32, shots=shots, ancillae=ancillae
|
|
237
|
+
)
|
|
238
|
+
label = "bell_pair_score"
|
|
239
|
+
else:
|
|
240
|
+
raise ValueError(f"unknown target: {target!r} (expected 'S' or 'bell_pair')")
|
|
241
|
+
|
|
242
|
+
n_params = 7 if ancillae else 3
|
|
243
|
+
best_params = tuple(0.0 for _ in range(n_params))
|
|
244
|
+
best_score = reward_fn(best_params)
|
|
245
|
+
print(f"start: {label}={best_score:.4f}")
|
|
246
|
+
|
|
247
|
+
t_start = time.time()
|
|
248
|
+
step = initial_step
|
|
249
|
+
iteration = 0
|
|
250
|
+
while True:
|
|
251
|
+
if max_iterations is not None and iteration >= max_iterations:
|
|
252
|
+
break
|
|
253
|
+
if time_budget is not None and (time.time() - t_start) >= time_budget:
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
candidate = tuple(p + random.uniform(-step, step) for p in best_params)
|
|
257
|
+
score = reward_fn(candidate)
|
|
258
|
+
if score > best_score:
|
|
259
|
+
best_score = score
|
|
260
|
+
best_params = candidate
|
|
261
|
+
print(
|
|
262
|
+
f"iter {iteration}: improved -> "
|
|
263
|
+
f"params={[round(p, 4) for p in candidate]} {label}={score:.4f}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
iteration += 1
|
|
267
|
+
if iteration % decay_every == 0:
|
|
268
|
+
step *= step_decay
|
|
269
|
+
|
|
270
|
+
print()
|
|
271
|
+
print(f"FINAL params: {[round(p, 6) for p in best_params]}")
|
|
272
|
+
print(f"FINAL {label}: {best_score:.4f}")
|
|
273
|
+
if target == "S":
|
|
274
|
+
print(f" (classical bound: 2.0, Tsirelson: {2*math.sqrt(2):.4f})")
|
|
275
|
+
print(f"total iterations: {iteration}")
|
|
276
|
+
return best_params, best_score
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def write_params_to_backend_file(params):
|
|
280
|
+
"""Patch the installed pyqrack/qrack_ace_backend.py in place, so the
|
|
281
|
+
fitted correction persists for future processes too, without
|
|
282
|
+
requiring a separate model-loading step at import time. params is
|
|
283
|
+
a 3-tuple (u only) or a 7-tuple (u + mcu)."""
|
|
284
|
+
import os
|
|
285
|
+
import re
|
|
286
|
+
|
|
287
|
+
path = os.path.join(os.path.dirname(_ace_mod.__file__), "qrack_ace_backend.py")
|
|
288
|
+
with open(path, "r") as f:
|
|
289
|
+
src = f.read()
|
|
290
|
+
|
|
291
|
+
theta, phi, lam = params[0], params[1], params[2]
|
|
292
|
+
src = re.sub(r"_BSEQ_THETA\s*=\s*(None|[-0-9.eE]+)", f"_BSEQ_THETA = {theta!r}", src)
|
|
293
|
+
src = re.sub(r"_BSEQ_PHI\s*=\s*(None|[-0-9.eE]+)", f"_BSEQ_PHI = {phi!r}", src)
|
|
294
|
+
src = re.sub(r"_BSEQ_LAMBDA\s*=\s*(None|[-0-9.eE]+)", f"_BSEQ_LAMBDA = {lam!r}", src)
|
|
295
|
+
|
|
296
|
+
if len(params) >= 7:
|
|
297
|
+
mcu_theta, mcu_phi, mcu_lam, mcu_gam = params[3], params[4], params[5], params[6]
|
|
298
|
+
src = re.sub(
|
|
299
|
+
r"_BSEQ_MCU_THETA\s*=\s*(None|[-0-9.eE]+)",
|
|
300
|
+
f"_BSEQ_MCU_THETA = {mcu_theta!r}",
|
|
301
|
+
src,
|
|
302
|
+
)
|
|
303
|
+
src = re.sub(
|
|
304
|
+
r"_BSEQ_MCU_PHI\s*=\s*(None|[-0-9.eE]+)", f"_BSEQ_MCU_PHI = {mcu_phi!r}", src
|
|
305
|
+
)
|
|
306
|
+
src = re.sub(
|
|
307
|
+
r"_BSEQ_MCU_LAMBDA\s*=\s*(None|[-0-9.eE]+)",
|
|
308
|
+
f"_BSEQ_MCU_LAMBDA = {mcu_lam!r}",
|
|
309
|
+
src,
|
|
310
|
+
)
|
|
311
|
+
src = re.sub(
|
|
312
|
+
r"_BSEQ_MCU_GAMMA\s*=\s*(None|[-0-9.eE]+)",
|
|
313
|
+
f"_BSEQ_MCU_GAMMA = {mcu_gam!r}",
|
|
314
|
+
src,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
with open(path, "w") as f:
|
|
318
|
+
f.write(src)
|
|
319
|
+
print(f"Wrote fitted angles into {path}")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def main():
|
|
323
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
324
|
+
parser.add_argument("--iterations", type=int, default=None)
|
|
325
|
+
parser.add_argument("--time-budget", type=float, default=200.0)
|
|
326
|
+
parser.add_argument("--repeats", type=int, default=4)
|
|
327
|
+
parser.add_argument("--shots", type=int, default=64)
|
|
328
|
+
parser.add_argument("--seed", type=int, default=None)
|
|
329
|
+
parser.add_argument(
|
|
330
|
+
"--target",
|
|
331
|
+
choices=["S", "bell_pair"],
|
|
332
|
+
default="bell_pair",
|
|
333
|
+
help="Optimization objective: CHSH S directly, or the simpler "
|
|
334
|
+
"Bell-pair correlation/balance/variance objective (default; "
|
|
335
|
+
"responds far more reliably to this correction mechanism in "
|
|
336
|
+
"direct testing).",
|
|
337
|
+
)
|
|
338
|
+
parser.add_argument(
|
|
339
|
+
"--no-ancillae",
|
|
340
|
+
dest="ancillae",
|
|
341
|
+
action="store_false",
|
|
342
|
+
default=True,
|
|
343
|
+
help="Fit only the 3-parameter u layer (no ancilla qubits "
|
|
344
|
+
"allocated at all, QrackAceBackend constructed with "
|
|
345
|
+
"bseq_ancillae=False). Default: fit all 7 parameters (u layer "
|
|
346
|
+
"+ ancilla/mcu layer), with bseq_ancillae=True.",
|
|
347
|
+
)
|
|
348
|
+
parser.add_argument(
|
|
349
|
+
"--persist",
|
|
350
|
+
action="store_true",
|
|
351
|
+
help="Write the fitted angles into the installed backend file.",
|
|
352
|
+
)
|
|
353
|
+
args = parser.parse_args()
|
|
354
|
+
|
|
355
|
+
params, score = optimize(
|
|
356
|
+
n_repeats=args.repeats,
|
|
357
|
+
shots=args.shots,
|
|
358
|
+
time_budget=args.time_budget,
|
|
359
|
+
max_iterations=args.iterations,
|
|
360
|
+
seed=args.seed,
|
|
361
|
+
target=args.target,
|
|
362
|
+
ancillae=args.ancillae,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if args.persist:
|
|
366
|
+
write_params_to_backend_file(params)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
if __name__ == "__main__":
|
|
370
|
+
main()
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
# Empirical optimizer for QrackAceBackend's BSEQ (Bell/CHSH) ancilla
|
|
2
|
-
# correction gate.
|
|
3
|
-
#
|
|
4
|
-
# The correction is a single, genuine controlled-U(theta, phi, lambda)
|
|
5
|
-
# gate (QrackSimulator.mcu), with each boundary qubit's ancilla as
|
|
6
|
-
# control and the boundary qubit itself as target. It is applied
|
|
7
|
-
# directly to the live quantum state -- before any probability is read
|
|
8
|
-
# -- every time the boundary qubit's state is corrected (prob() / m() /
|
|
9
|
-
# force_m() all funnel through this), so it composes correctly even
|
|
10
|
-
# while the qubit's Bell-pair partner is still genuinely superposed.
|
|
11
|
-
#
|
|
12
|
-
# The 3 angles (QrackAceBackend._BSEQ_THETA/_PHI/_LAMBDA) are the only
|
|
13
|
-
# trainable parameters. They are fit here by direct, gradient-free
|
|
14
|
-
# search against the empirically measured CHSH S statistic (or its 4
|
|
15
|
-
# components individually), averaged over repeated Bell-pair trials to
|
|
16
|
-
# average down the substantial per-construction measurement variance
|
|
17
|
-
# inherent to this architecture (each circuit construction makes its
|
|
18
|
-
# own one-time classical collapse choice early in the elision pipeline;
|
|
19
|
-
# averaging over MANY independent constructions is what makes the mean
|
|
20
|
-
# S a meaningful, optimizable target -- a single fixed gate cannot
|
|
21
|
-
# reduce per-shot variance, since that variance is set by an earlier,
|
|
22
|
-
# already-classical decision the gate cannot retroactively undo).
|
|
23
|
-
#
|
|
24
|
-
# Usage:
|
|
25
|
-
# python3 optimize_bseq_correction.py [--iterations N] [--time-budget SECONDS]
|
|
26
|
-
#
|
|
27
|
-
# Writes the found angles directly into pyqrack's installed
|
|
28
|
-
# qrack_ace_backend.py (QrackAceBackend._BSEQ_THETA/_PHI/_LAMBDA), so
|
|
29
|
-
# every subsequently-constructed QrackAceBackend in the running
|
|
30
|
-
# process (and, if you choose to persist the file, future processes)
|
|
31
|
-
# uses the fitted correction automatically -- no separate model file,
|
|
32
|
-
# no extra inference dependency. The correction is 3 floats; baking
|
|
33
|
-
# them in as class attributes is already the most efficient possible
|
|
34
|
-
# packaging, and needs no JIT/torch artifact at inference time.
|
|
35
|
-
|
|
36
|
-
import argparse
|
|
37
|
-
import math
|
|
38
|
-
import random
|
|
39
|
-
import statistics
|
|
40
|
-
import time
|
|
41
|
-
|
|
42
|
-
from pyqrack import QrackAceBackend, Pauli
|
|
43
|
-
import pyqrack.qrack_ace_backend as _ace_mod
|
|
44
|
-
|
|
45
|
-
PI = math.pi
|
|
46
|
-
|
|
47
|
-
# Standard CHSH angle set.
|
|
48
|
-
THETA_A, THETA_AP = 0.0, PI / 2
|
|
49
|
-
THETA_B, THETA_BP = PI / 4, -PI / 4
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _apply_measurement_basis(sim, q, theta):
|
|
53
|
-
sim.r(Pauli.PauliY, theta, q)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _measure_expectation(sim, shots):
|
|
57
|
-
results = sim.measure_shots([0, 1], shots)
|
|
58
|
-
total = 0
|
|
59
|
-
for res in results:
|
|
60
|
-
a = (res >> 0) & 1
|
|
61
|
-
b = (res >> 1) & 1
|
|
62
|
-
total += 1 if a == b else -1
|
|
63
|
-
return total / shots
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _expectation(theta1, theta2, shots, width=16, long_range_columns=1):
|
|
67
|
-
"""One fresh Bell-pair-with-ancilla construction, measured at a given
|
|
68
|
-
basis-angle pair. This is exactly the bseq.py pattern: h(0), cx(0,1),
|
|
69
|
-
then a PauliY rotation on each half before measuring."""
|
|
70
|
-
s = QrackAceBackend(width, long_range_columns=long_range_columns)
|
|
71
|
-
s.h(0)
|
|
72
|
-
s.mcx([0], 1)
|
|
73
|
-
_apply_measurement_basis(s, 0, theta1)
|
|
74
|
-
_apply_measurement_basis(s, 1, theta2)
|
|
75
|
-
return _measure_expectation(s, shots)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def compute_chsh_components(shots, **kwargs):
|
|
79
|
-
"""Return the 4 CHSH correlator terms (E(a,b), E(a,b'), E(a',b), E(a',b'))."""
|
|
80
|
-
return (
|
|
81
|
-
_expectation(THETA_A, THETA_B, shots, **kwargs),
|
|
82
|
-
_expectation(THETA_A, THETA_BP, shots, **kwargs),
|
|
83
|
-
_expectation(THETA_AP, THETA_B, shots, **kwargs),
|
|
84
|
-
_expectation(THETA_AP, THETA_BP, shots, **kwargs),
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def compute_S(shots, **kwargs):
|
|
89
|
-
e_ab, e_abp, e_apb, e_apbp = compute_chsh_components(shots, **kwargs)
|
|
90
|
-
return e_ab + e_abp + e_apb - e_apbp
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def set_correction(theta, phi, lam):
|
|
94
|
-
_ace_mod.QrackAceBackend._BSEQ_THETA = theta
|
|
95
|
-
_ace_mod.QrackAceBackend._BSEQ_PHI = phi
|
|
96
|
-
_ace_mod.QrackAceBackend._BSEQ_LAMBDA = lam
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def mean_reward(theta, phi, lam, n_repeats, shots, **kwargs):
|
|
100
|
-
"""The actual optimization target: mean S across many independent
|
|
101
|
-
Bell-pair-with-ancilla constructions ("an RCS data set for Bell-pair
|
|
102
|
-
perturbations of the ancillae"), not a single noisy reading."""
|
|
103
|
-
set_correction(theta, phi, lam)
|
|
104
|
-
vals = [compute_S(shots, **kwargs) for _ in range(n_repeats)]
|
|
105
|
-
return statistics.mean(vals)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def optimize(
|
|
109
|
-
n_repeats=4,
|
|
110
|
-
shots=64,
|
|
111
|
-
time_budget=200.0,
|
|
112
|
-
max_iterations=None,
|
|
113
|
-
seed=None,
|
|
114
|
-
initial_step=PI / 4,
|
|
115
|
-
step_decay=0.8,
|
|
116
|
-
decay_every=10,
|
|
117
|
-
):
|
|
118
|
-
"""Gradient-free random-walk search over (theta, phi, lambda),
|
|
119
|
-
maximizing mean_reward. Measurement sampling is not differentiable,
|
|
120
|
-
so this uses repeated empirical evaluation rather than backprop --
|
|
121
|
-
appropriate given the actual cost here is 3 scalars, not a network.
|
|
122
|
-
"""
|
|
123
|
-
if seed is not None:
|
|
124
|
-
random.seed(seed)
|
|
125
|
-
|
|
126
|
-
best_params = (0.0, 0.0, 0.0)
|
|
127
|
-
best_score = mean_reward(*best_params, n_repeats, shots)
|
|
128
|
-
print(f"start: mean_S={best_score:.4f}")
|
|
129
|
-
|
|
130
|
-
t_start = time.time()
|
|
131
|
-
step = initial_step
|
|
132
|
-
iteration = 0
|
|
133
|
-
while True:
|
|
134
|
-
if max_iterations is not None and iteration >= max_iterations:
|
|
135
|
-
break
|
|
136
|
-
if time_budget is not None and (time.time() - t_start) >= time_budget:
|
|
137
|
-
break
|
|
138
|
-
|
|
139
|
-
candidate = tuple(p + random.uniform(-step, step) for p in best_params)
|
|
140
|
-
score = mean_reward(*candidate, n_repeats, shots)
|
|
141
|
-
if score > best_score:
|
|
142
|
-
best_score = score
|
|
143
|
-
best_params = candidate
|
|
144
|
-
print(
|
|
145
|
-
f"iter {iteration}: improved -> "
|
|
146
|
-
f"params={[round(p, 4) for p in candidate]} mean_S={score:.4f}"
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
iteration += 1
|
|
150
|
-
if iteration % decay_every == 0:
|
|
151
|
-
step *= step_decay
|
|
152
|
-
|
|
153
|
-
print()
|
|
154
|
-
print(f"FINAL params: {[round(p, 6) for p in best_params]}")
|
|
155
|
-
print(f"FINAL mean_S: {best_score:.4f} (classical bound: 2.0, Tsirelson: {2*math.sqrt(2):.4f})")
|
|
156
|
-
print(f"total iterations: {iteration}")
|
|
157
|
-
return best_params, best_score
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def write_params_to_backend_file(theta, phi, lam):
|
|
161
|
-
"""Patch the installed pyqrack/qrack_ace_backend.py in place, so the
|
|
162
|
-
fitted correction persists for future processes too, without
|
|
163
|
-
requiring a separate model-loading step at import time."""
|
|
164
|
-
import os
|
|
165
|
-
import re
|
|
166
|
-
|
|
167
|
-
path = os.path.join(os.path.dirname(_ace_mod.__file__), "qrack_ace_backend.py")
|
|
168
|
-
with open(path, "r") as f:
|
|
169
|
-
src = f.read()
|
|
170
|
-
|
|
171
|
-
src = re.sub(r"_BSEQ_THETA\s*=\s*[-0-9.eE]+", f"_BSEQ_THETA = {theta!r}", src)
|
|
172
|
-
src = re.sub(r"_BSEQ_PHI\s*=\s*[-0-9.eE]+", f"_BSEQ_PHI = {phi!r}", src)
|
|
173
|
-
src = re.sub(r"_BSEQ_LAMBDA\s*=\s*[-0-9.eE]+", f"_BSEQ_LAMBDA = {lam!r}", src)
|
|
174
|
-
|
|
175
|
-
with open(path, "w") as f:
|
|
176
|
-
f.write(src)
|
|
177
|
-
print(f"Wrote fitted angles into {path}")
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def main():
|
|
181
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
182
|
-
parser.add_argument("--iterations", type=int, default=None)
|
|
183
|
-
parser.add_argument("--time-budget", type=float, default=200.0)
|
|
184
|
-
parser.add_argument("--repeats", type=int, default=4)
|
|
185
|
-
parser.add_argument("--shots", type=int, default=64)
|
|
186
|
-
parser.add_argument("--seed", type=int, default=None)
|
|
187
|
-
parser.add_argument(
|
|
188
|
-
"--persist",
|
|
189
|
-
action="store_true",
|
|
190
|
-
help="Write the fitted angles into the installed backend file.",
|
|
191
|
-
)
|
|
192
|
-
args = parser.parse_args()
|
|
193
|
-
|
|
194
|
-
params, score = optimize(
|
|
195
|
-
n_repeats=args.repeats,
|
|
196
|
-
shots=args.shots,
|
|
197
|
-
time_budget=args.time_budget,
|
|
198
|
-
max_iterations=args.iterations,
|
|
199
|
-
seed=args.seed,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
if args.persist:
|
|
203
|
-
write_params_to_backend_file(*params)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if __name__ == "__main__":
|
|
207
|
-
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
|
|
File without changes
|
|
File without changes
|