pyqrack-cpu 2.2.3__tar.gz → 2.3.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.
Files changed (31) hide show
  1. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/MANIFEST.in +1 -0
  2. {pyqrack_cpu-2.2.3/pyqrack_cpu.egg-info → pyqrack_cpu-2.3.0}/PKG-INFO +1 -1
  3. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_ace_backend.py +416 -199
  4. pyqrack_cpu-2.3.0/pyqrack/scripts/optimize_bseq_correction.py +207 -0
  5. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0/pyqrack_cpu.egg-info}/PKG-INFO +1 -1
  6. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack_cpu.egg-info/SOURCES.txt +1 -0
  7. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/setup.py +1 -1
  8. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/LICENSE +0 -0
  9. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/Makefile +0 -0
  10. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/README.md +0 -0
  11. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyproject.toml +0 -0
  12. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/__init__.py +0 -0
  13. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/neuron_activation_fn.py +0 -0
  14. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/pauli.py +0 -0
  15. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_circuit.py +0 -0
  16. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  17. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_neuron.py +0 -0
  18. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  19. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_simulator.py +0 -0
  20. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_stabilizer.py +0 -0
  21. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_system/__init__.py +0 -0
  22. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/qrack_system/qrack_system.py +0 -0
  23. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/quimb_circuit_type.py +0 -0
  24. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/stats/__init__.py +0 -0
  25. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/stats/load_quantized_data.py +0 -0
  26. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack/stats/quantize_by_range.py +0 -0
  27. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack_cpu.egg-info/dependency_links.txt +0 -0
  28. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack_cpu.egg-info/not-zip-safe +0 -0
  29. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack_cpu.egg-info/requires.txt +0 -0
  30. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/pyqrack_cpu.egg-info/top_level.txt +0 -0
  31. {pyqrack_cpu-2.2.3 → pyqrack_cpu-2.3.0}/setup.cfg +0 -0
@@ -1,3 +1,4 @@
1
1
  include pyqrack/qrack_system/qrack_lib/*
2
2
  include pyqrack/qrack_system/qrack_cl_precompile/*
3
+ include pyqrack/scripts/*
3
4
  include Makefile
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu
3
- Version: 2.2.3
3
+ Version: 2.3.0
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -28,6 +28,39 @@ 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
+
31
64
 
32
65
  # Initial stub and concept produced through conversation with Elara
33
66
  # (the custom OpenAI GPT)
@@ -191,7 +224,6 @@ def _cpauli_lhv(prob, targ, axis, anti, theta=math.pi):
191
224
  elif axis == Pauli.PauliZ:
192
225
  targ.rz(effective_theta)
193
226
 
194
-
195
227
  class QrackAceBackend:
196
228
  """A back end for elided quantum error correction
197
229
 
@@ -212,6 +244,12 @@ class QrackAceBackend:
212
244
  is_transpose(bool): Rows are long if False, columns are long if True
213
245
  """
214
246
 
247
+ # Sweepable vote weights for the 4-source boundary correction pool:
248
+ # [slot0, slot1, slot2, lhv]. Must sum to an odd number so the
249
+ # underlying hard-vote tally (independent of the continuous RMS
250
+ # formula) is always tie-free.
251
+ _LHV_VOTE_WEIGHTS = [2, 1, 1, 1]
252
+
215
253
  def __init__(
216
254
  self,
217
255
  qubit_count=1,
@@ -276,7 +314,24 @@ class QrackAceBackend:
276
314
  self._is_row_long_range[-1] = False
277
315
  sim_count = col_patch_count * row_patch_count
278
316
 
317
+ # Boundary qubits no longer carry a private classical LHV proxy.
318
+ # Instead, every boundary site (row- and/or column-boundary alike)
319
+ # gets a real qubit in one single, shared "crossbar" QrackSimulator.
320
+ # That simulator's own greedy elision (set_sdrp) is trusted to
321
+ # automatically factor apart whatever boundary sites turn out to be
322
+ # separable (e.g. disjoint rails, rail intersections), exactly the
323
+ # same way it already factors apart unentangled subspaces within
324
+ # any other single QrackSimulator instance. We don't need to special
325
+ # -case "crossbar intersections" by hand; the elision does it for us.
326
+ boundary_sim_id = sim_count
327
+ boundary_count = 0
328
+
279
329
  self._qubits = []
330
+ self._lhv = {}
331
+ 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 {}
280
335
  sim_counts = [0] * sim_count
281
336
  sim_id = 0
282
337
  tot_qubits = 0
@@ -290,8 +345,21 @@ class QrackAceBackend:
290
345
  qubit.append((t_sim_id, sim_counts[t_sim_id]))
291
346
  sim_counts[t_sim_id] += 1
292
347
 
293
- qubit.append(
294
- LHVQubit(to_clone=(to_clone._qubits[tot_qubits][2] if to_clone else None))
348
+ qubit.append((boundary_sim_id, boundary_count))
349
+ boundary_count += 1
350
+
351
+ # Reserve a second boundary-sim qubit as the ancilla,
352
+ # right alongside slot2. It starts fresh (|0>) and is
353
+ # Bell-paired with slot2 lazily, the first time slot2
354
+ # is actually touched by a real coupling gate (see
355
+ # _apply_coupling), so it taps slot2's true evolving
356
+ # state rather than a disconnected pair prepared at
357
+ # construction time.
358
+ self._ancilla[tot_qubits] = (boundary_sim_id, boundary_count)
359
+ boundary_count += 1
360
+
361
+ self._lhv[tot_qubits] = LHVQubit(
362
+ to_clone=(to_clone._lhv[tot_qubits] if to_clone else None)
295
363
  )
296
364
 
297
365
  if (not c) and (not r):
@@ -309,8 +377,23 @@ class QrackAceBackend:
309
377
  self._qubits.append(qubit)
310
378
  tot_qubits += 1
311
379
 
380
+ # The crossbar's size is fixed by how many boundary sites exist.
381
+ # When there are none (e.g. a grid small enough, relative to
382
+ # long_range_rows/columns, that the whole thing is "fully
383
+ # connected" with no QEC boundary at all), we must NOT allocate a
384
+ # 0-qubit QrackSimulator for the crossbar: a 0-qubit QrackSimulator
385
+ # can be constructed, but calling .clone() on one crashes the
386
+ # native Qrack core (segfault), and clone() is called routinely by
387
+ # measure_shots()/clone(). So the boundary sim is only created when
388
+ # boundary_count > 0, exactly mirroring how the original LHV-based
389
+ # code never instantiated anything for the boundary case when
390
+ # there were no boundary sites.
391
+ has_boundary = boundary_count > 0
392
+ if has_boundary:
393
+ sim_counts.append(boundary_count)
394
+
312
395
  self.sim = []
313
- for i in range(sim_count):
396
+ for i in range(sim_count + (1 if has_boundary else 0)):
314
397
  self.sim.append(
315
398
  to_clone.sim[i].clone()
316
399
  if to_clone
@@ -326,11 +409,13 @@ class QrackAceBackend:
326
409
  )
327
410
  )
328
411
 
329
- # You can still call "set_sdrp," after the constructor.
412
+ # You can still "set_sdrp" later, after the constructor.
330
413
  # if "QRACK_QUNIT_SEPARABILITY_THRESHOLD" not in os.environ:
331
414
  # # (1 - 1 / sqrt(2)) / 4 (but empirically tuned)
332
415
  # self.sim[i].set_sdrp(0.073223304703363119)
333
416
 
417
+ self._boundary_sim_id = boundary_sim_id if has_boundary else None
418
+
334
419
  def clone(self):
335
420
  return QrackAceBackend(to_clone=self)
336
421
 
@@ -405,6 +490,24 @@ class QrackAceBackend:
405
490
  p1 = self.sim[q1[0]].prob(q1[1]) if isinstance(q1, tuple) else q1.prob()
406
491
  p2 = self.sim[q2[0]].prob(q2[1]) if isinstance(q2, tuple) else q2.prob()
407
492
 
493
+ # When p1 and p2 are within floating-point noise of each other
494
+ # (self._epsilon), they carry no real information about which
495
+ # qubit is "more likely 1" -- e.g. a fresh target right after
496
+ # _cx_shadow's H() always reads ~0.5, indistinguishably from a
497
+ # genuinely-mixed control. This shadow mechanism can only return
498
+ # ONE definite classical pick per call, so resolving every such
499
+ # near-tie to a FIXED qubit (as a plain "<" or "<=" comparison
500
+ # would, once p1/p2 are within epsilon of each other) reintroduces
501
+ # a systematic bias -- it just moves the bias from "favors q1" to
502
+ # "always favors q2" instead of removing it. The aggregate
503
+ # statistics this shadow is meant to approximate (e.g. a real CX
504
+ # from a maximally-mixed control onto a fresh target makes BOTH
505
+ # qubits individually read prob=0.5, perfectly correlated) are
506
+ # only reproduced, across many circuit instances, if a genuine
507
+ # tie is broken at random rather than by a fixed rule.
508
+ if abs(p1 - p2) <= self._epsilon:
509
+ return (p1, q2) if random.random() < 0.5 else (p2, q1)
510
+
408
511
  if p1 < p2:
409
512
  return p2, q1
410
513
 
@@ -412,7 +515,16 @@ class QrackAceBackend:
412
515
 
413
516
  def _cz_shadow(self, q1, q2):
414
517
  prob_max, t = self._ct_pair_prob(q1, q2)
415
- if prob_max > 0.5:
518
+ # NOTE: this must be ">=", not ">". H() applied to any qubit in a
519
+ # definite computational-basis state (e.g. a fresh boundary/ancilla
520
+ # qubit, as in _cx_shadow's H-sandwich) lands at EXACTLY prob=0.5,
521
+ # deterministically -- this is the ordinary case, not a rare
522
+ # floating-point tie. A strict "> 0.5" therefore silently no-ops
523
+ # the shadow gate every time it targets a fresh qubit, which
524
+ # breaks entanglement transfer for the extremely common case of a
525
+ # CX/CY/CZ from a maximally-mixed-looking control onto a fresh
526
+ # target (e.g. H(0); cx(0,1) for a Bell pair).
527
+ if prob_max >= (0.5 - self._epsilon):
416
528
  if isinstance(t, tuple):
417
529
  self.sim[t[0]].z(t[1])
418
530
  else:
@@ -472,35 +584,23 @@ class QrackAceBackend:
472
584
 
473
585
  @staticmethod
474
586
  def _get_qb_lhv_indices(hq):
475
- qb = []
587
+ # Historically, index 2 (when present) pointed at a private
588
+ # classical LHVQubit proxy and had to be special-cased everywhere.
589
+ # It is now an ordinary (sim_id, idx) tuple into the shared
590
+ # boundary "crossbar" QrackSimulator, so it is just one more
591
+ # coupling target like every other index. We keep this helper's
592
+ # name and signature for minimal call-site churn; "lhv" is now
593
+ # always -1 (no index needs special-casing any more).
476
594
  if len(hq) < 2:
477
595
  qb = [0]
478
- lhv = -1
479
596
  elif len(hq) < 4:
480
- qb = [0, 1]
481
- lhv = 2
597
+ qb = [0, 1, 2]
482
598
  else:
483
- qb = [0, 1, 3, 4]
484
- lhv = 2
599
+ qb = [0, 1, 2, 3, 4]
600
+ lhv = -1
485
601
 
486
602
  return qb, lhv
487
603
 
488
- @staticmethod
489
- def _get_lhv_bloch_angles(sim):
490
- # Z axis
491
- z = sim.bloch[2]
492
-
493
- # X axis
494
- x = sim.bloch[0]
495
-
496
- # Y axis
497
- y = sim.bloch[1]
498
-
499
- inclination = math.atan2(math.sqrt(x**2 + y**2), z)
500
- azimuth = math.atan2(y, x)
501
-
502
- return azimuth, inclination
503
-
504
604
  def _get_bloch_angles(self, hq):
505
605
  sim = self.sim[hq[0]].clone()
506
606
  q = hq[1]
@@ -543,9 +643,17 @@ class QrackAceBackend:
543
643
 
544
644
  sim.mtrx([m00, m01, m10, m11], q)
545
645
 
646
+ @staticmethod
647
+ def _get_lhv_bloch_angles(sim):
648
+ z = sim.bloch[2]
649
+ x = sim.bloch[0]
650
+ y = sim.bloch[1]
651
+ inclination = math.atan2(math.sqrt(x**2 + y**2), z)
652
+ azimuth = math.atan2(y, x)
653
+ return azimuth, inclination
654
+
546
655
  @staticmethod
547
656
  def _rotate_lhv_to_bloch(sim, delta_azimuth, delta_inclination):
548
- # Apply rotation as "Azimuth, Inclination" (AI)
549
657
  cosA = math.cos(delta_azimuth)
550
658
  sinA = math.sin(delta_azimuth)
551
659
  cosI = math.cos(delta_inclination / 2)
@@ -558,27 +666,68 @@ class QrackAceBackend:
558
666
 
559
667
  sim.mtrx([m00, m01, m10, m11])
560
668
 
669
+ # Trainable BSEQ correction: a genuine controlled-U(theta,phi,lambda)
670
+ # gate (QrackSimulator.mcu), with the ancilla as control and the
671
+ # boundary qubit (slot2) as target -- both live in the same boundary
672
+ # simulator, so this is a real, coherent two-qubit unitary, not a
673
+ # classical shadow approximation. Applied directly to the quantum
674
+ # state with no probability read or collapsed first, so it composes
675
+ # correctly even while the Bell-pair partner is still genuinely
676
+ # superposed. These 3 angles are the only trainable parameters;
677
+ # fit them empirically against the measured BSEQ S statistic (see
678
+ # the companion training/optimization script), not hand-derived.
679
+ _BSEQ_THETA = 0.5524356880065896
680
+ _BSEQ_PHI = 0.5611771084538236
681
+ _BSEQ_LAMBDA = -0.3706652126771467
682
+
683
+ def _apply_bseq_correction(self, lq):
684
+ """Apply the trained controlled-U BSEQ correction, if applicable.
685
+
686
+ Fires every time this boundary qubit's state is about to be read
687
+ out (prob()/_correct()/m()/force_m() all funnel through here, so
688
+ this covers every simulator-API readout for the qubit). A no-op
689
+ whenever there's no ancilla for this site, or it hasn't been
690
+ Bell-paired with slot2 yet (nothing to condition on).
691
+ """
692
+ anc = self._ancilla.get(lq)
693
+ if anc is None or lq not in self._ancilla_paired:
694
+ return
695
+ hq = self._unpack(lq)
696
+ if len(hq) < 3:
697
+ return
698
+ anc_sim, anc_idx = anc
699
+ slot2_sim, slot2_idx = hq[2]
700
+ if anc_sim != slot2_sim:
701
+ return
702
+ self.sim[anc_sim].mcu(
703
+ [anc_idx],
704
+ slot2_idx,
705
+ QrackAceBackend._BSEQ_THETA,
706
+ QrackAceBackend._BSEQ_PHI,
707
+ QrackAceBackend._BSEQ_LAMBDA,
708
+ )
709
+
561
710
  def _correct(self, lq, phase=False, skip_rotation=False):
562
711
  hq = self._unpack(lq)
563
712
 
564
713
  if len(hq) == 1:
565
714
  return
566
715
 
567
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
716
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
568
717
 
569
718
  if phase:
570
719
  for q in qb:
571
720
  b = hq[q]
572
721
  self.sim[b[0]].h(b[1])
573
- b = hq[lhv]
574
- b.h()
722
+ if lq in self._lhv:
723
+ self._lhv[lq].h()
575
724
 
576
725
  if len(hq) == 5:
577
726
  # RMS
578
727
  p = [
579
728
  self.sim[hq[0][0]].prob(hq[0][1]),
580
729
  self.sim[hq[1][0]].prob(hq[1][1]),
581
- hq[2].prob(),
730
+ self.sim[hq[2][0]].prob(hq[2][1]),
582
731
  self.sim[hq[3][0]].prob(hq[3][1]),
583
732
  self.sim[hq[4][0]].prob(hq[4][1]),
584
733
  ]
@@ -598,7 +747,7 @@ class QrackAceBackend:
598
747
  result = (
599
748
  (random.random() < 0.5)
600
749
  if abs(eff_prob - 0.5) <= self._epsilon
601
- else (eff_prob > 0.5)
750
+ else (eff_prob >= 0.5)
602
751
  )
603
752
  syndrome = (
604
753
  [1 - p[0], 1 - p[1], 1 - p[2], 1 - p[3], 1 - p[4]]
@@ -607,91 +756,121 @@ class QrackAceBackend:
607
756
  )
608
757
  for q in range(5):
609
758
  if syndrome[q] > (0.5 + self._epsilon):
610
- if q == 2:
611
- hq[q].x()
612
- else:
613
- self.sim[hq[q][0]].x(hq[q][1])
759
+ self.sim[hq[q][0]].x(hq[q][1])
614
760
 
615
761
  if not skip_rotation:
616
762
  a, i = [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]
617
763
  a[0], i[0] = self._get_bloch_angles(hq[0])
618
764
  a[1], i[1] = self._get_bloch_angles(hq[1])
619
- a[2], i[2] = QrackAceBackend._get_lhv_bloch_angles(hq[2])
765
+ a[2], i[2] = self._get_bloch_angles(hq[2])
620
766
  a[3], i[3] = self._get_bloch_angles(hq[3])
621
767
  a[4], i[4] = self._get_bloch_angles(hq[4])
622
768
 
623
- a_target = 0
624
- i_target = 0
769
+ a_target = sum(a) / 5
770
+ i_target = sum(i) / 5
625
771
  for x in range(5):
626
- if x == 2:
627
- continue
628
- a_target += a[x]
629
- i_target += i[x]
630
-
631
- a_target /= 5
632
- i_target /= 5
633
- for x in range(5):
634
- if x == 2:
635
- QrackAceBackend._rotate_lhv_to_bloch(
636
- hq[x], a_target - a[x], i_target - i[x]
637
- )
638
- else:
639
- self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
772
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
640
773
 
641
774
  else:
642
- # RMS
643
- p = [
644
- self.sim[hq[0][0]].prob(hq[0][1]),
645
- self.sim[hq[1][0]].prob(hq[1][1]),
646
- hq[2].prob(),
647
- ]
648
- # Balancing suggestion from Elara (the custom OpenAI GPT)
649
- prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
650
- qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
651
- eff_prob = (prms + (1 - qrms)) / 2
652
- result = (
653
- (random.random() < 0.5)
654
- if abs(eff_prob - 0.5) <= self._epsilon
655
- else (eff_prob > 0.5)
656
- )
657
- syndrome = [1 - p[0], 1 - p[1], 1 - p[2]] if result else [p[0], p[1], p[2]]
658
- for q in range(3):
659
- if syndrome[q] > (0.5 + self._epsilon):
660
- if q == 2:
661
- hq[q].x()
662
- else:
775
+ lhv = self._lhv.get(lq)
776
+ if lhv is None:
777
+ # RMS
778
+ p = [
779
+ self.sim[hq[0][0]].prob(hq[0][1]),
780
+ self.sim[hq[1][0]].prob(hq[1][1]),
781
+ self.sim[hq[2][0]].prob(hq[2][1]),
782
+ ]
783
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
784
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
785
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
786
+ eff_prob = (prms + (1 - qrms)) / 2
787
+ result = (
788
+ (random.random() < 0.5)
789
+ if abs(eff_prob - 0.5) <= self._epsilon
790
+ else (eff_prob >= 0.5)
791
+ )
792
+ syndrome = [1 - p[0], 1 - p[1], 1 - p[2]] if result else [p[0], p[1], p[2]]
793
+ for q in range(3):
794
+ if syndrome[q] > (0.5 + self._epsilon):
663
795
  self.sim[hq[q][0]].x(hq[q][1])
796
+ else:
797
+ # Conditional tie-breaking cascade, NOT a fixed-weight pool.
798
+ # A fixed weight on slot0/lhv pulls every decision toward
799
+ # 0.5 even when slot1 and slot2 already agree confidently,
800
+ # because slot0 and lhv are legitimately, permanently near
801
+ # 0.5 for genuinely-entangled topologies (e.g. when slot0
802
+ # shares a simulator with the control) -- a "vote" stuck at
803
+ # 0.5 is not neutral in an RMS pool, it actively drags the
804
+ # result toward the center. The actual intent ("LHV serves
805
+ # only to act as a tie-breaker") is a conditional, not a
806
+ # weight: trust slot1/slot2 alone whenever they agree, and
807
+ # only consult slot0, then lhv, when they genuinely don't.
808
+ #
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
+ p0 = self.sim[hq[0][0]].prob(hq[0][1])
820
+ p1 = self.sim[hq[1][0]].prob(hq[1][1])
821
+ p2 = self.sim[hq[2][0]].prob(hq[2][1])
822
+ p_lhv = lhv.prob()
823
+
824
+ if (p1 >= 0.5) == (p2 >= 0.5):
825
+ prms = math.sqrt((p1**2 + p2**2) / 2)
826
+ qrms = math.sqrt(((1 - p1) ** 2 + (1 - p2) ** 2) / 2)
827
+ eff_prob = (prms + (1 - qrms)) / 2
828
+ result = (
829
+ (random.random() < 0.5)
830
+ if abs(eff_prob - 0.5) <= self._epsilon
831
+ else (eff_prob >= 0.5)
832
+ )
833
+ elif abs(p0 - 0.5) > self._epsilon:
834
+ # Genuine deadlock between slot1/slot2; slot0 (a real,
835
+ # exactly-entangled qubit in this common topology) has
836
+ # a real opinion, so it breaks the tie.
837
+ result = p0 >= 0.5
838
+ else:
839
+ # slot0 is itself ambiguous; only now does the LHV's
840
+ # continuous, non-collapsing proxy actually decide.
841
+ result = (
842
+ (random.random() < 0.5)
843
+ if abs(p_lhv - 0.5) <= self._epsilon
844
+ else (p_lhv >= 0.5)
845
+ )
846
+
847
+ p = [p0, p1, p2]
848
+ syndrome = [1 - x for x in p] if result else list(p)
849
+ for q in range(3):
850
+ if syndrome[q] > (0.5 + self._epsilon):
851
+ self.sim[hq[q][0]].x(hq[q][1])
852
+ # The LHV proxy is never hard-collapsed via x(); it is only
853
+ # ever updated by its own transversal gate evolution and
854
+ # _cpauli_lhv, preserving the property that makes it usable
855
+ # as a non-collapsing tie-breaker of last resort.
664
856
 
665
857
  if not skip_rotation:
666
858
  a, i = [0, 0, 0], [0, 0, 0]
667
859
  a[0], i[0] = self._get_bloch_angles(hq[0])
668
860
  a[1], i[1] = self._get_bloch_angles(hq[1])
669
- a[2], i[2] = QrackAceBackend._get_lhv_bloch_angles(hq[2])
670
-
671
- a_target = 0
672
- i_target = 0
673
- for x in range(3):
674
- if x == 2:
675
- continue
676
- a_target += a[x]
677
- i_target += i[x]
861
+ a[2], i[2] = self._get_bloch_angles(hq[2])
678
862
 
679
- a_target /= 3
680
- i_target /= 3
863
+ a_target = sum(a) / 3
864
+ i_target = sum(i) / 3
681
865
  for x in range(3):
682
- if x == 2:
683
- QrackAceBackend._rotate_lhv_to_bloch(
684
- hq[x], a_target - a[x], i_target - i[x]
685
- )
686
- else:
687
- self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
866
+ self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
688
867
 
689
868
  if phase:
690
869
  for q in qb:
691
870
  b = hq[q]
692
871
  self.sim[b[0]].h(b[1])
693
- b = hq[lhv]
694
- b.h()
872
+ if lq in self._lhv:
873
+ self._lhv[lq].h()
695
874
 
696
875
  def apply_magnetic_bias(self, q, b):
697
876
  if b == 0:
@@ -699,22 +878,13 @@ class QrackAceBackend:
699
878
  b = math.exp(b)
700
879
  for x in q:
701
880
  hq = self._unpack(x)
702
- for c in range(len(hq)):
703
- h = hq[c]
704
- if c == 2:
705
- a, i = QrackAceBackend._get_lhv_bloch_angles(h)
706
- QrackAceBackend._rotate_lhv_to_bloch(
707
- h,
708
- math.atan(math.tan(a) * b) - a,
709
- math.atan(math.tan(i) * b) - i,
710
- )
711
- else:
712
- a, i = self._get_bloch_angles(h)
713
- self._rotate_to_bloch(
714
- h,
715
- math.atan(math.tan(a) * b) - a,
716
- math.atan(math.tan(i) * b) - i,
717
- )
881
+ for h in hq:
882
+ a, i = self._get_bloch_angles(h)
883
+ self._rotate_to_bloch(
884
+ h,
885
+ math.atan(math.tan(a) * b) - a,
886
+ math.atan(math.tan(i) * b) - i,
887
+ )
718
888
 
719
889
  def u(self, lq, th, ph, lm):
720
890
  hq = self._unpack(lq)
@@ -723,37 +893,42 @@ class QrackAceBackend:
723
893
  self.sim[b[0]].u(b[1], th, ph, lm)
724
894
  return
725
895
 
726
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
896
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
727
897
 
728
898
  for q in qb:
729
899
  b = hq[q]
730
900
  self.sim[b[0]].u(b[1], th, ph, lm)
731
901
 
732
- b = hq[lhv]
733
- b.u(th, ph, lm)
902
+ lhv = self._lhv.get(lq)
903
+ if lhv is not None:
904
+ lhv.u(th, ph, lm)
734
905
 
735
906
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
736
907
 
737
908
  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
+
738
912
  hq = self._unpack(lq)
739
913
  if len(hq) < 2:
740
914
  b = hq[0]
741
915
  self.sim[b[0]].r(p, th, b[1])
742
916
  return
743
917
 
744
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
918
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
745
919
 
746
920
  for q in qb:
747
921
  b = hq[q]
748
922
  self.sim[b[0]].r(p, th, b[1])
749
923
 
750
- b = hq[lhv]
751
- if p == Pauli.PauliX:
752
- b.rx(th)
753
- elif p == Pauli.PauliY:
754
- b.ry(th)
755
- elif p == Pauli.PauliZ:
756
- b.rz(th)
924
+ lhv = self._lhv.get(lq)
925
+ if lhv is not None:
926
+ if p == Pauli.PauliX:
927
+ lhv.rx(th)
928
+ elif p == Pauli.PauliY:
929
+ lhv.ry(th)
930
+ elif p == Pauli.PauliZ:
931
+ lhv.rz(th)
757
932
 
758
933
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
759
934
 
@@ -766,14 +941,15 @@ class QrackAceBackend:
766
941
 
767
942
  self._correct(lq)
768
943
 
769
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
944
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
770
945
 
771
946
  for q in qb:
772
947
  b = hq[q]
773
948
  self.sim[b[0]].h(b[1])
774
949
 
775
- b = hq[lhv]
776
- b.h()
950
+ lhv = self._lhv.get(lq)
951
+ if lhv is not None:
952
+ lhv.h()
777
953
 
778
954
  # Correction deferred to next 2-qubit gate (_cpauli calls _correct)
779
955
 
@@ -784,14 +960,15 @@ class QrackAceBackend:
784
960
  self.sim[b[0]].s(b[1])
785
961
  return
786
962
 
787
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
963
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
788
964
 
789
965
  for q in qb:
790
966
  b = hq[q]
791
967
  self.sim[b[0]].s(b[1])
792
968
 
793
- b = hq[lhv]
794
- b.s()
969
+ lhv = self._lhv.get(lq)
970
+ if lhv is not None:
971
+ lhv.s()
795
972
 
796
973
  def adjs(self, lq):
797
974
  hq = self._unpack(lq)
@@ -800,14 +977,15 @@ class QrackAceBackend:
800
977
  self.sim[b[0]].adjs(b[1])
801
978
  return
802
979
 
803
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
980
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
804
981
 
805
982
  for q in qb:
806
983
  b = hq[q]
807
984
  self.sim[b[0]].adjs(b[1])
808
985
 
809
- b = hq[lhv]
810
- b.adjs()
986
+ lhv = self._lhv.get(lq)
987
+ if lhv is not None:
988
+ lhv.adjs()
811
989
 
812
990
  def x(self, lq):
813
991
  hq = self._unpack(lq)
@@ -816,14 +994,15 @@ class QrackAceBackend:
816
994
  self.sim[b[0]].x(b[1])
817
995
  return
818
996
 
819
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
997
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
820
998
 
821
999
  for q in qb:
822
1000
  b = hq[q]
823
1001
  self.sim[b[0]].x(b[1])
824
1002
 
825
- b = hq[lhv]
826
- b.x()
1003
+ lhv = self._lhv.get(lq)
1004
+ if lhv is not None:
1005
+ lhv.x()
827
1006
 
828
1007
  def y(self, lq):
829
1008
  hq = self._unpack(lq)
@@ -832,14 +1011,15 @@ class QrackAceBackend:
832
1011
  self.sim[b[0]].y(b[1])
833
1012
  return
834
1013
 
835
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1014
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
836
1015
 
837
1016
  for q in qb:
838
1017
  b = hq[q]
839
1018
  self.sim[b[0]].y(b[1])
840
1019
 
841
- b = hq[lhv]
842
- b.y()
1020
+ lhv = self._lhv.get(lq)
1021
+ if lhv is not None:
1022
+ lhv.y()
843
1023
 
844
1024
  def z(self, lq):
845
1025
  hq = self._unpack(lq)
@@ -848,14 +1028,15 @@ class QrackAceBackend:
848
1028
  self.sim[b[0]].z(b[1])
849
1029
  return
850
1030
 
851
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1031
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
852
1032
 
853
1033
  for q in qb:
854
1034
  b = hq[q]
855
1035
  self.sim[b[0]].z(b[1])
856
1036
 
857
- b = hq[lhv]
858
- b.z()
1037
+ lhv = self._lhv.get(lq)
1038
+ if lhv is not None:
1039
+ lhv.z()
859
1040
 
860
1041
  def t(self, lq):
861
1042
  hq = self._unpack(lq)
@@ -864,14 +1045,15 @@ class QrackAceBackend:
864
1045
  self.sim[b[0]].t(b[1])
865
1046
  return
866
1047
 
867
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1048
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
868
1049
 
869
1050
  for q in qb:
870
1051
  b = hq[q]
871
1052
  self.sim[b[0]].t(b[1])
872
1053
 
873
- b = hq[lhv]
874
- b.t()
1054
+ lhv = self._lhv.get(lq)
1055
+ if lhv is not None:
1056
+ lhv.t()
875
1057
 
876
1058
  def adjt(self, lq):
877
1059
  hq = self._unpack(lq)
@@ -880,14 +1062,15 @@ class QrackAceBackend:
880
1062
  self.sim[b[0]].adjt(b[1])
881
1063
  return
882
1064
 
883
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1065
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
884
1066
 
885
1067
  for q in qb:
886
1068
  b = hq[q]
887
1069
  self.sim[b[0]].adjt(b[1])
888
1070
 
889
- b = hq[lhv]
890
- b.adjt()
1071
+ lhv = self._lhv.get(lq)
1072
+ if lhv is not None:
1073
+ lhv.adjt()
891
1074
 
892
1075
  def _get_gate(self, pauli, anti, sim_id):
893
1076
  gate = None
@@ -927,15 +1110,11 @@ class QrackAceBackend:
927
1110
 
928
1111
  return connected, boundary
929
1112
 
930
- def _apply_coupling(self, pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr):
1113
+ def _apply_coupling(self, pauli, anti, qb1, hq1, qb2, hq2, lq1_lr):
931
1114
  for q1 in qb1:
932
- if q1 == lhv1:
933
- continue
934
1115
  b1 = hq1[q1]
935
1116
  gate_fn, shadow_fn = self._get_gate(pauli, anti, b1[0])
936
1117
  for q2 in qb2:
937
- if q2 == lhv2:
938
- continue
939
1118
  b2 = hq2[q2]
940
1119
  if b1[0] == b2[0]:
941
1120
  gate_fn([b1[1]], b2[1])
@@ -956,18 +1135,36 @@ class QrackAceBackend:
956
1135
 
957
1136
  self._correct(lq1)
958
1137
 
959
- qb1, lhv1 = QrackAceBackend._get_qb_lhv_indices(hq1)
960
- qb2, lhv2 = QrackAceBackend._get_qb_lhv_indices(hq2)
961
- # Apply cross coupling on hardware qubits first
962
- self._apply_coupling(pauli, anti, qb1, lhv1, hq1, qb2, lhv2, hq2, lq1_lr)
963
- # Apply coupling to the local-hidden-variable target
964
- if lhv2 >= 0:
965
- _cpauli_lhv(
966
- hq1[lhv1].prob() if lhv1 >= 0 else self.sim[hq1[0][0]].prob(hq1[0][1]),
967
- hq2[lhv2],
968
- pauli,
969
- anti,
970
- )
1138
+ qb1, _ = QrackAceBackend._get_qb_lhv_indices(hq1)
1139
+ qb2, _ = QrackAceBackend._get_qb_lhv_indices(hq2)
1140
+ # Apply cross coupling on every qubit, including former-LHV boundary
1141
+ # qubits, which now live as real qubits in the shared boundary sim.
1142
+ self._apply_coupling(pauli, anti, qb1, hq1, qb2, hq2, lq1_lr)
1143
+
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
+ if lq2 in self._lhv:
1166
+ ctrl_prob = self.sim[hq1[0][0]].prob(hq1[0][1])
1167
+ _cpauli_lhv(ctrl_prob, self._lhv[lq2], pauli, anti)
971
1168
 
972
1169
  self._correct(lq1, True)
973
1170
  if pauli != Pauli.PauliZ:
@@ -1064,7 +1261,7 @@ class QrackAceBackend:
1064
1261
  p = [
1065
1262
  self.sim[hq[0][0]].prob(hq[0][1]),
1066
1263
  self.sim[hq[1][0]].prob(hq[1][1]),
1067
- hq[2].prob(),
1264
+ self.sim[hq[2][0]].prob(hq[2][1]),
1068
1265
  self.sim[hq[3][0]].prob(hq[3][1]),
1069
1266
  self.sim[hq[4][0]].prob(hq[4][1]),
1070
1267
  ]
@@ -1081,15 +1278,32 @@ class QrackAceBackend:
1081
1278
  / 7
1082
1279
  )
1083
1280
  else:
1084
- # RMS
1085
- p = [
1086
- self.sim[hq[0][0]].prob(hq[0][1]),
1087
- self.sim[hq[1][0]].prob(hq[1][1]),
1088
- hq[2].prob(),
1089
- ]
1090
- # Balancing suggestion from Elara (the custom OpenAI GPT)
1091
- prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
1092
- qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
1281
+ lhv = self._lhv.get(lq)
1282
+ if lhv is None:
1283
+ # RMS
1284
+ p = [
1285
+ self.sim[hq[0][0]].prob(hq[0][1]),
1286
+ self.sim[hq[1][0]].prob(hq[1][1]),
1287
+ self.sim[hq[2][0]].prob(hq[2][1]),
1288
+ ]
1289
+ # Balancing suggestion from Elara (the custom OpenAI GPT)
1290
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
1291
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
1292
+ else:
1293
+ p = [
1294
+ self.sim[hq[0][0]].prob(hq[0][1]),
1295
+ self.sim[hq[1][0]].prob(hq[1][1]),
1296
+ self.sim[hq[2][0]].prob(hq[2][1]),
1297
+ ]
1298
+ # The three real replicas are already mutually consistent
1299
+ # here (the _correct() call above already ran the
1300
+ # conditional tie-breaking cascade and forced agreement via
1301
+ # x()). Re-weighting in the LHV here would reintroduce the
1302
+ # same center-dragging distortion the cascade was built to
1303
+ # avoid -- a plain RMS over the now-settled replicas is the
1304
+ # correct, already-decided answer.
1305
+ prms = math.sqrt((p[0] ** 2 + p[1] ** 2 + p[2] ** 2) / 3)
1306
+ qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2 + (1 - p[2]) ** 2) / 3)
1093
1307
 
1094
1308
  return (prms + (1 - qrms)) / 2
1095
1309
 
@@ -1100,9 +1314,9 @@ class QrackAceBackend:
1100
1314
  return self.sim[b[0]].m(b[1])
1101
1315
 
1102
1316
  p = self.prob(lq)
1103
- result = ((p + self._epsilon) >= 1.0) or (random.random() < p)
1317
+ result = ((p + self._epsilon) >= 1) or (random.random() < p)
1104
1318
 
1105
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1319
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
1106
1320
 
1107
1321
  for q in qb:
1108
1322
  b = hq[q]
@@ -1113,10 +1327,11 @@ class QrackAceBackend:
1113
1327
  else:
1114
1328
  self.sim[b[0]].force_m(b[1], result)
1115
1329
 
1116
- b = hq[lhv]
1117
- b.reset()
1118
- if result:
1119
- b.x()
1330
+ lhv = self._lhv.get(lq)
1331
+ if lhv is not None:
1332
+ lhv.reset()
1333
+ if result:
1334
+ lhv.x()
1120
1335
 
1121
1336
  return result
1122
1337
 
@@ -1128,7 +1343,7 @@ class QrackAceBackend:
1128
1343
 
1129
1344
  self._correct(lq)
1130
1345
 
1131
- qb, lhv = QrackAceBackend._get_qb_lhv_indices(hq)
1346
+ qb, _ = QrackAceBackend._get_qb_lhv_indices(hq)
1132
1347
 
1133
1348
  for q in qb:
1134
1349
  b = hq[q]
@@ -1139,10 +1354,11 @@ class QrackAceBackend:
1139
1354
  else:
1140
1355
  self.sim[b[0]].force_m(b[1], result)
1141
1356
 
1142
- b = hq[lhv]
1143
- b.reset()
1144
- if result:
1145
- b.x()
1357
+ lhv = self._lhv.get(lq)
1358
+ if lhv is not None:
1359
+ lhv.reset()
1360
+ if result:
1361
+ lhv.x()
1146
1362
 
1147
1363
  return result
1148
1364
 
@@ -1509,25 +1725,22 @@ class QrackAceBackend:
1509
1725
  return self._coupling_map
1510
1726
 
1511
1727
  coupling_map = set()
1512
- # self._row_length is the number of qubits per row (the row's span,
1513
- # i.e. the column count); self._col_length is the number of rows.
1514
- num_rows, num_cols = self._col_length, self._row_length
1728
+ rows, cols = self._row_length, self._col_length
1515
1729
 
1516
- # Row-major logical index, strided by the actual row span.
1730
+ # Map each column index to its full list of logical qubit indices
1517
1731
  def logical_index(row, col):
1518
- return row * num_cols + col
1732
+ return row * cols + col
1519
1733
 
1520
- for row in range(num_rows):
1521
- connected_rows, _ = self._get_connected(row, True)
1522
- for col in range(num_cols):
1523
- connected_cols, _ = self._get_connected(col, False)
1734
+ for col in range(cols):
1735
+ connected_cols, _ = self._get_connected(col, False)
1736
+ for row in range(rows):
1737
+ connected_rows, _ = self._get_connected(row, False)
1524
1738
  a = logical_index(row, col)
1525
- for r in connected_rows:
1526
- for c in connected_cols:
1739
+ for c in connected_cols:
1740
+ for r in connected_rows:
1527
1741
  b = logical_index(r, c)
1528
1742
  if a != b:
1529
1743
  coupling_map.add((a, b))
1530
- coupling_map.add((b, a))
1531
1744
 
1532
1745
  self._coupling_map = sorted(coupling_map)
1533
1746
 
@@ -1541,11 +1754,9 @@ class QrackAceBackend:
1541
1754
  )
1542
1755
  noise_model = NoiseModel()
1543
1756
 
1757
+ # Single-qubit depolarizing only on boundary qubits
1544
1758
  boundary_qubits = set()
1545
1759
  for a, b in self.get_logical_coupling_map():
1546
- if a > b:
1547
- #Skip duplicates
1548
- continue
1549
1760
  col_a = a % self._row_length
1550
1761
  col_b = b % self._row_length
1551
1762
  is_long_a = self._is_col_long_range[col_a]
@@ -1556,6 +1767,12 @@ class QrackAceBackend:
1556
1767
  if not is_long_b:
1557
1768
  boundary_qubits.add(b)
1558
1769
 
1770
+ for q in boundary_qubits:
1771
+ for gate in ["u", "u1", "u2", "u3", "h", "x", "y", "z",
1772
+ "s", "sdg", "t", "tdg", "rx", "ry", "rz"]:
1773
+ noise_model.add_quantum_error(
1774
+ depolarizing_error(x, 1), gate, [q])
1775
+
1559
1776
  # Two-qubit depolarizing on boundary-crossing and boundary-adjacent gates
1560
1777
  for a, b in self.get_logical_coupling_map():
1561
1778
  col_a = a % self._row_length
@@ -0,0 +1,207 @@
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu
3
- Version: 2.2.3
3
+ Version: 2.3.0
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -17,6 +17,7 @@ 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
20
21
  pyqrack/stats/__init__.py
21
22
  pyqrack/stats/load_quantized_data.py
22
23
  pyqrack/stats/quantize_by_range.py
@@ -7,7 +7,7 @@ from setuptools import setup
7
7
  from setuptools.command.build_py import build_py
8
8
 
9
9
 
10
- VERSION = "2.2.3"
10
+ VERSION = "2.3.0"
11
11
 
12
12
  # Read long description from README.
13
13
  README_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'README.md')
File without changes
File without changes
File without changes
File without changes
File without changes