pyqrack-cpu-complex128 2.26.6__tar.gz → 2.28.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/Makefile +1 -1
- {pyqrack_cpu_complex128-2.26.6/pyqrack_cpu_complex128.egg-info → pyqrack_cpu_complex128-2.28.0}/PKG-INFO +1 -1
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/__init__.py +1 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_ace_backend.py +258 -65
- pyqrack_cpu_complex128-2.28.0/pyqrack/qrack_ace_mps_backend.py +392 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0/pyqrack_cpu_complex128.egg-info}/PKG-INFO +1 -1
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/SOURCES.txt +1 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/setup.py +1 -1
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/LICENSE +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/MANIFEST.in +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/README.md +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyproject.toml +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/neuron_activation_fn.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/pauli.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_circuit.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_neuron.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_neuron_torch_layer.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_simulator.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_stabilizer.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_system/__init__.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_system/qrack_system.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/quimb_circuit_type.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/__init__.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/load_quantized_data.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/quantize_by_range.py +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/dependency_links.txt +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/not-zip-safe +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/requires.txt +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/top_level.txt +0 -0
- {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/setup.cfg +0 -0
|
@@ -30,7 +30,7 @@ build-deps:
|
|
|
30
30
|
rm -rf pyqrack/qrack_system/qrack_cl_precompile
|
|
31
31
|
ifneq ($(OS),Windows_NT)
|
|
32
32
|
ifeq ($(QRACK_PRESENT),)
|
|
33
|
-
git clone https://github.com/unitaryfund/qrack.git; cd qrack; git checkout
|
|
33
|
+
git clone https://github.com/unitaryfund/qrack.git; cd qrack; git checkout 5c5a54c0d44b6039e57de770f57a37b6e8caeafa; cd ..
|
|
34
34
|
endif
|
|
35
35
|
mkdir -p qrack/build
|
|
36
36
|
ifeq ($(UNAME_S),Linux)
|
|
@@ -7,6 +7,7 @@ from .pauli import Pauli
|
|
|
7
7
|
from .neuron_activation_fn import NeuronActivationFn
|
|
8
8
|
from .quimb_circuit_type import QuimbCircuitType
|
|
9
9
|
from .qrack_ace_backend import QrackAceBackend
|
|
10
|
+
from .qrack_ace_mps_backend import QrackAceMPSBackend, QrackMPSPatch
|
|
10
11
|
from .qrack_near_clifford_qec_backend import QrackNearCliffordQecBackend
|
|
11
12
|
from .qrack_circuit import QrackCircuit
|
|
12
13
|
from .qrack_neuron import QrackNeuron
|
{pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_ace_backend.py
RENAMED
|
@@ -252,6 +252,7 @@ class QrackAceBackend:
|
|
|
252
252
|
is_1d_chain=False,
|
|
253
253
|
is_error_detection=True,
|
|
254
254
|
is_boundary_repetition_code=False,
|
|
255
|
+
use_crossbars=True,
|
|
255
256
|
to_clone=None,
|
|
256
257
|
):
|
|
257
258
|
if to_clone:
|
|
@@ -263,6 +264,7 @@ class QrackAceBackend:
|
|
|
263
264
|
is_1d_chain = to_clone.is_1d_chain
|
|
264
265
|
is_error_detection = to_clone.is_error_detection
|
|
265
266
|
is_boundary_repetition_code = to_clone.is_boundary_repetition_code
|
|
267
|
+
use_crossbars = to_clone.use_crossbars
|
|
266
268
|
if qubit_count < 0:
|
|
267
269
|
qubit_count = 0
|
|
268
270
|
if long_range_columns < 0:
|
|
@@ -275,6 +277,19 @@ class QrackAceBackend:
|
|
|
275
277
|
self.is_transpose = is_transpose
|
|
276
278
|
self.is_torus = is_torus
|
|
277
279
|
self.is_error_detection = is_error_detection
|
|
280
|
+
# Opt-in revival of the shared boundary "crossbar" QrackSimulator
|
|
281
|
+
# (see the note just below, at the qubit-allocation loop) that was
|
|
282
|
+
# previously removed outright for not scaling with patch count.
|
|
283
|
+
# Off by default -- same topology/behavior as before this option
|
|
284
|
+
# existed. On, it gives every boundary qubit one extra REAL replica
|
|
285
|
+
# in a single simulator shared by every boundary site in the whole
|
|
286
|
+
# grid, which is itself an un-scalable idea (that one simulator's
|
|
287
|
+
# qubit count grows with total boundary-qubit count, not with
|
|
288
|
+
# patch count) -- but for a small number of patches (e.g. 3-4,
|
|
289
|
+
# fitting on one GPU) it was measured to add real, genuine
|
|
290
|
+
# connectivity value on top of the classical LHV proxy, so it's
|
|
291
|
+
# kept available for exactly that regime rather than discarded.
|
|
292
|
+
self.use_crossbars = use_crossbars
|
|
278
293
|
self.is_boundary_repetition_code = is_boundary_repetition_code
|
|
279
294
|
|
|
280
295
|
if Qrack.fppow < 5:
|
|
@@ -334,19 +349,26 @@ class QrackAceBackend:
|
|
|
334
349
|
self._is_row_long_range[-1] = False
|
|
335
350
|
sim_count = col_patch_count * row_patch_count
|
|
336
351
|
|
|
337
|
-
# The shared boundary "crossbar" QrackSimulator
|
|
338
|
-
#
|
|
339
|
-
# alike) one extra real replica in a single
|
|
340
|
-
# boundary qubits, trusting that
|
|
341
|
-
# (set_sdrp) to automatically
|
|
342
|
-
#
|
|
343
|
-
# intersection" logic.
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
#
|
|
352
|
+
# The shared boundary "crossbar" QrackSimulator, self.use_crossbars
|
|
353
|
+
# (see the constructor): every boundary site (row- and/or column-
|
|
354
|
+
# boundary alike) gets one extra real replica in a single
|
|
355
|
+
# simulator shared by ALL boundary qubits, trusting that
|
|
356
|
+
# simulator's own greedy elision (set_sdrp) to automatically
|
|
357
|
+
# factor apart whatever boundary sites turn out to be separable,
|
|
358
|
+
# rather than hand-coding "crossbar intersection" logic. Measured
|
|
359
|
+
# to add real connectivity value, but the single shared simulator
|
|
360
|
+
# grows with total boundary-qubit count rather than patch count,
|
|
361
|
+
# so it doesn't scale -- off by default, and worth turning on only
|
|
362
|
+
# for a handful of patches. Every boundary qubit ALSO still gets
|
|
363
|
+
# its classical LHVQubit proxy (self._lhv, below) regardless, same
|
|
364
|
+
# as when this option is off: with the crossbar on, the proxy
|
|
365
|
+
# drops to being only the last-resort tie-breaker _correct() falls
|
|
366
|
+
# back to when even the crossbar replica is itself ambiguous; with
|
|
367
|
+
# it off, the proxy is promoted to cover the crossbar's old
|
|
368
|
+
# tie-breaking role directly.
|
|
369
|
+
boundary_sim_id = sim_count
|
|
370
|
+
boundary_count = 0
|
|
371
|
+
|
|
350
372
|
self._qubits = []
|
|
351
373
|
self._lhv = {}
|
|
352
374
|
sim_counts = [0] * sim_count
|
|
@@ -362,6 +384,10 @@ class QrackAceBackend:
|
|
|
362
384
|
qubit.append((t_sim_id, sim_counts[t_sim_id]))
|
|
363
385
|
sim_counts[t_sim_id] += 1
|
|
364
386
|
|
|
387
|
+
if self.use_crossbars:
|
|
388
|
+
qubit.append((boundary_sim_id, boundary_count))
|
|
389
|
+
boundary_count += 1
|
|
390
|
+
|
|
365
391
|
if (not to_clone) or (tot_qubits in to_clone._lhv):
|
|
366
392
|
self._lhv[tot_qubits] = LHVQubit(
|
|
367
393
|
to_clone=(to_clone._lhv[tot_qubits] if to_clone else None)
|
|
@@ -384,6 +410,18 @@ class QrackAceBackend:
|
|
|
384
410
|
)
|
|
385
411
|
tot_qubits += 1
|
|
386
412
|
|
|
413
|
+
# The crossbar's size is fixed by how many boundary sites exist.
|
|
414
|
+
# When there are none at all (e.g. a grid small enough, relative
|
|
415
|
+
# to long_range_rows/columns, to be fully connected with no QEC
|
|
416
|
+
# boundary), we must NOT allocate a 0-qubit QrackSimulator for it
|
|
417
|
+
# -- sim_count/sim_counts simply stay exactly as they were,
|
|
418
|
+
# exactly mirroring how every other per-simulator allocation below
|
|
419
|
+
# already guards for "nothing to allocate."
|
|
420
|
+
has_boundary = self.use_crossbars and (boundary_count > 0)
|
|
421
|
+
if has_boundary:
|
|
422
|
+
sim_counts.append(boundary_count)
|
|
423
|
+
sim_count += 1
|
|
424
|
+
|
|
387
425
|
# Error-detection gadget (IBM-style detect-and-post-select, not
|
|
388
426
|
# correction): one shared ancilla per simulator -- one per patch,
|
|
389
427
|
# "a single ancilla per patch, overall" -- reused across every real
|
|
@@ -460,25 +498,34 @@ class QrackAceBackend:
|
|
|
460
498
|
# "Golden value"
|
|
461
499
|
self._sdrp = 0.0
|
|
462
500
|
|
|
501
|
+
sim_kwargs = dict(
|
|
502
|
+
is_schmidt_decompose_multi=is_schmidt_decompose_multi,
|
|
503
|
+
is_stabilizer_hybrid=is_stabilizer_hybrid,
|
|
504
|
+
is_binary_decision_tree=is_binary_decision_tree,
|
|
505
|
+
is_gpu=is_gpu,
|
|
506
|
+
is_host_pointer=is_host_pointer,
|
|
507
|
+
is_near_clifford_tableau_writer=is_near_clifford_tableau_writer,
|
|
508
|
+
noise=noise,
|
|
509
|
+
)
|
|
463
510
|
self.sim = []
|
|
464
511
|
for i in range(sim_count):
|
|
465
512
|
self.sim.append(
|
|
466
513
|
to_clone.sim[i].clone()
|
|
467
514
|
if to_clone
|
|
468
|
-
else
|
|
469
|
-
sim_counts[i],
|
|
470
|
-
is_schmidt_decompose_multi=is_schmidt_decompose_multi,
|
|
471
|
-
is_stabilizer_hybrid=is_stabilizer_hybrid,
|
|
472
|
-
is_binary_decision_tree=is_binary_decision_tree,
|
|
473
|
-
is_gpu=is_gpu,
|
|
474
|
-
is_host_pointer=is_host_pointer,
|
|
475
|
-
is_near_clifford_tableau_writer=is_near_clifford_tableau_writer,
|
|
476
|
-
noise=noise,
|
|
477
|
-
)
|
|
515
|
+
else self._new_patch_sim(sim_counts[i], sim_kwargs)
|
|
478
516
|
)
|
|
479
517
|
|
|
518
|
+
def _new_patch_sim(self, qubit_count, sim_kwargs):
|
|
519
|
+
# Factory for one patch's "just-in-time state machine." Nothing
|
|
520
|
+
# else in this class depends on the patch being a QrackSimulator:
|
|
521
|
+
# any object exposing the same gate/prob/m/force_m/clone interface
|
|
522
|
+
# can be substituted by overriding this (see QrackAceMPSBackend).
|
|
523
|
+
return QrackSimulator(qubit_count, **sim_kwargs)
|
|
524
|
+
|
|
480
525
|
def clone(self):
|
|
481
|
-
|
|
526
|
+
# type(self), not QrackAceBackend, so subclasses survive cloning
|
|
527
|
+
# (measure_shots() and run_qiskit_circuit() clone constantly).
|
|
528
|
+
return type(self)(to_clone=self)
|
|
482
529
|
|
|
483
530
|
def set_sdrp(self, sdrp):
|
|
484
531
|
sdrp = min(1, sdrp)
|
|
@@ -729,16 +776,19 @@ class QrackAceBackend:
|
|
|
729
776
|
|
|
730
777
|
@staticmethod
|
|
731
778
|
def _get_qb_lhv_indices(hq):
|
|
732
|
-
#
|
|
733
|
-
#
|
|
734
|
-
#
|
|
735
|
-
#
|
|
736
|
-
#
|
|
737
|
-
#
|
|
738
|
-
#
|
|
739
|
-
#
|
|
740
|
-
#
|
|
741
|
-
#
|
|
779
|
+
# Every entry of hq is an ordinary (sim_id, idx) tuple into a real
|
|
780
|
+
# physical replica: 1 for a bulk qubit; with use_crossbars=False
|
|
781
|
+
# (default), 2 for an edge-boundary qubit (home + adjacent-patch)
|
|
782
|
+
# or 4 for a corner-boundary qubit (home + adjacent-patch + 2
|
|
783
|
+
# diagonal-patch); with use_crossbars=True, 3 (home + adjacent-
|
|
784
|
+
# patch + shared crossbar) or 5 (home + adjacent-patch + shared
|
|
785
|
+
# crossbar + 2 diagonal-patch). None of them are virtual/LHV-
|
|
786
|
+
# backed slots -- the classical LHVQubit proxy (self._lhv) is
|
|
787
|
+
# tracked entirely separately from hq, not as one of its indices
|
|
788
|
+
# -- so qb is simply every index, in order, regardless of which
|
|
789
|
+
# of those counts applies. We keep this helper's name and
|
|
790
|
+
# signature for minimal call-site churn; "lhv" is always -1 (no
|
|
791
|
+
# index needs special-casing).
|
|
742
792
|
qb = list(range(len(hq)))
|
|
743
793
|
lhv = -1
|
|
744
794
|
|
|
@@ -945,6 +995,164 @@ class QrackAceBackend:
|
|
|
945
995
|
# all -- skip the rotation rather than rotate toward an
|
|
946
996
|
# arbitrary/undefined target derived from noise.
|
|
947
997
|
|
|
998
|
+
elif len(hq) == 5:
|
|
999
|
+
# Corner-boundary qubit, self.use_crossbars=True: 5 real
|
|
1000
|
+
# replicas (home, adjacent-patch, the shared crossbar at
|
|
1001
|
+
# slot 2, and the 2 diagonal-patch shadows). Structurally
|
|
1002
|
+
# identical to the no-crossbar 4-replica case above -- the 4
|
|
1003
|
+
# "end-cap" replicas [p0, p1, p3, p4] vote via their own RMS
|
|
1004
|
+
# pool first; a genuine 2-2 split among them defers to the
|
|
1005
|
+
# crossbar replica (hq[2], a REAL qubit, not a classical
|
|
1006
|
+
# proxy); the classical LHV proxy is consulted only if the
|
|
1007
|
+
# crossbar itself also turns out ambiguous.
|
|
1008
|
+
p0 = self.sim[hq[0][0]].prob(hq[0][1])
|
|
1009
|
+
p1 = self.sim[hq[1][0]].prob(hq[1][1])
|
|
1010
|
+
p2 = self.sim[hq[2][0]].prob(hq[2][1])
|
|
1011
|
+
p3 = self.sim[hq[3][0]].prob(hq[3][1])
|
|
1012
|
+
p4 = self.sim[hq[4][0]].prob(hq[4][1])
|
|
1013
|
+
lhv = self._lhv.get(lq)
|
|
1014
|
+
|
|
1015
|
+
end_caps = [p0, p1, p3, p4]
|
|
1016
|
+
high_count = sum(1 for x in end_caps if x > (0.5 + self._epsilon))
|
|
1017
|
+
low_count = sum(1 for x in end_caps if x < (0.5 - self._epsilon))
|
|
1018
|
+
undecided_count = len(end_caps) - high_count - low_count
|
|
1019
|
+
end_caps_tied = (high_count == 2 and low_count == 2) or (undecided_count > 0)
|
|
1020
|
+
|
|
1021
|
+
if not end_caps_tied:
|
|
1022
|
+
prms = math.sqrt(sum(x**2 for x in end_caps) / 4)
|
|
1023
|
+
qrms = math.sqrt(sum((1 - x) ** 2 for x in end_caps) / 4)
|
|
1024
|
+
eff_prob = (prms + (1 - qrms)) / 2
|
|
1025
|
+
result = (
|
|
1026
|
+
(random.random() < 0.5)
|
|
1027
|
+
if abs(eff_prob - 0.5) <= self._epsilon
|
|
1028
|
+
else (eff_prob >= 0.5)
|
|
1029
|
+
)
|
|
1030
|
+
elif abs(p2 - 0.5) > self._epsilon:
|
|
1031
|
+
result = p2 >= 0.5
|
|
1032
|
+
elif lhv is not None:
|
|
1033
|
+
p_lhv = lhv.prob()
|
|
1034
|
+
result = (
|
|
1035
|
+
(random.random() < 0.5)
|
|
1036
|
+
if abs(p_lhv - 0.5) <= self._epsilon
|
|
1037
|
+
else (p_lhv >= 0.5)
|
|
1038
|
+
)
|
|
1039
|
+
else:
|
|
1040
|
+
result = random.random() < 0.5
|
|
1041
|
+
|
|
1042
|
+
p = [p0, p1, p2, p3, p4]
|
|
1043
|
+
syndrome = [1 - x for x in p] if result else list(p)
|
|
1044
|
+
for q in range(5):
|
|
1045
|
+
if syndrome[q] > (0.5 + self._epsilon):
|
|
1046
|
+
self.sim[hq[q][0]].x(hq[q][1])
|
|
1047
|
+
|
|
1048
|
+
if not skip_rotation:
|
|
1049
|
+
a, i, rs = [0] * 5, [0] * 5, [0] * 5
|
|
1050
|
+
for idx in range(5):
|
|
1051
|
+
a[idx], i[idx], rs[idx] = self._get_bloch_angles(hq[idx])
|
|
1052
|
+
w = [1 - r for r in rs]
|
|
1053
|
+
|
|
1054
|
+
w_total = sum(w)
|
|
1055
|
+
if w_total > self._epsilon:
|
|
1056
|
+
a_target = sum(wx * ax for wx, ax in zip(w, a)) / w_total
|
|
1057
|
+
i_target = sum(wx * ix for wx, ix in zip(w, i)) / w_total
|
|
1058
|
+
# Same separability gating as the 4-replica case
|
|
1059
|
+
# above (hq[0] excluded outright, hq[1:] only
|
|
1060
|
+
# rotated while still reasonably separable).
|
|
1061
|
+
for x in range(1, 5):
|
|
1062
|
+
if rs[x] <= self._rot_epsilon:
|
|
1063
|
+
self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
|
|
1064
|
+
|
|
1065
|
+
elif len(hq) == 3:
|
|
1066
|
+
# Edge-boundary qubit, self.use_crossbars=True: 3 real
|
|
1067
|
+
# replicas (home, adjacent-patch, and the shared crossbar at
|
|
1068
|
+
# slot 2). Same conditional tie-breaking cascade as the
|
|
1069
|
+
# no-crossbar 2-replica case below, but with the crossbar
|
|
1070
|
+
# (hq[2], a REAL qubit) taking over the role the classical
|
|
1071
|
+
# LHV proxy plays there: trust slot1 (adjacent-patch) and
|
|
1072
|
+
# slot2 (crossbar) together whenever they agree; fall back
|
|
1073
|
+
# to slot0 (home) when they disagree and it has a decisive
|
|
1074
|
+
# opinion; consult the classical LHV proxy only as an
|
|
1075
|
+
# absolute last resort, if even slot0 is ambiguous.
|
|
1076
|
+
p0 = self.sim[hq[0][0]].prob(hq[0][1])
|
|
1077
|
+
p1 = self.sim[hq[1][0]].prob(hq[1][1])
|
|
1078
|
+
p2 = self.sim[hq[2][0]].prob(hq[2][1])
|
|
1079
|
+
lhv = self._lhv.get(lq)
|
|
1080
|
+
|
|
1081
|
+
p1_high = p1 > (0.5 + self._epsilon)
|
|
1082
|
+
p1_low = p1 < (0.5 - self._epsilon)
|
|
1083
|
+
p2_high = p2 > (0.5 + self._epsilon)
|
|
1084
|
+
p2_low = p2 < (0.5 - self._epsilon)
|
|
1085
|
+
end_caps_agree = (p1_high and p2_high) or (p1_low and p2_low)
|
|
1086
|
+
slot0_disagrees_with_end_caps = (
|
|
1087
|
+
end_caps_agree
|
|
1088
|
+
and (abs(p0 - 0.5) > self._epsilon)
|
|
1089
|
+
and ((p0 >= 0.5) != p1_high)
|
|
1090
|
+
)
|
|
1091
|
+
if slot0_disagrees_with_end_caps:
|
|
1092
|
+
# Same reasoning as the no-crossbar 2-replica case below:
|
|
1093
|
+
# the pair agreeing is not, by itself, reliable
|
|
1094
|
+
# corroboration if slot0 has a real, decisive, opposing
|
|
1095
|
+
# opinion -- that contradiction is the signal the pair is
|
|
1096
|
+
# stale, not corroborating.
|
|
1097
|
+
result = p0 >= 0.5
|
|
1098
|
+
elif end_caps_agree:
|
|
1099
|
+
prms = math.sqrt((p1**2 + p2**2) / 2)
|
|
1100
|
+
qrms = math.sqrt(((1 - p1) ** 2 + (1 - p2) ** 2) / 2)
|
|
1101
|
+
eff_prob = (prms + (1 - qrms)) / 2
|
|
1102
|
+
result = (
|
|
1103
|
+
(random.random() < 0.5)
|
|
1104
|
+
if abs(eff_prob - 0.5) <= self._epsilon
|
|
1105
|
+
else (eff_prob >= 0.5)
|
|
1106
|
+
)
|
|
1107
|
+
elif abs(p0 - 0.5) > self._epsilon:
|
|
1108
|
+
# Genuine deadlock between slot1/slot2; slot0 (a real,
|
|
1109
|
+
# exactly-entangled qubit in this common topology) has a
|
|
1110
|
+
# real opinion, so it breaks the tie.
|
|
1111
|
+
result = p0 >= 0.5
|
|
1112
|
+
elif lhv is not None:
|
|
1113
|
+
# slot0 is itself ambiguous, and so is the crossbar pair
|
|
1114
|
+
# above it; only now does the classical LHV's continuous,
|
|
1115
|
+
# non-collapsing proxy actually decide, as the last
|
|
1116
|
+
# resort.
|
|
1117
|
+
p_lhv = lhv.prob()
|
|
1118
|
+
result = (
|
|
1119
|
+
(random.random() < 0.5)
|
|
1120
|
+
if abs(p_lhv - 0.5) <= self._epsilon
|
|
1121
|
+
else (p_lhv >= 0.5)
|
|
1122
|
+
)
|
|
1123
|
+
else:
|
|
1124
|
+
result = random.random() < 0.5
|
|
1125
|
+
|
|
1126
|
+
p = [p0, p1, p2]
|
|
1127
|
+
syndrome = [1 - x for x in p] if result else list(p)
|
|
1128
|
+
for q in range(3):
|
|
1129
|
+
if syndrome[q] > (0.5 + self._epsilon):
|
|
1130
|
+
self.sim[hq[q][0]].x(hq[q][1])
|
|
1131
|
+
# The LHV proxy is never hard-collapsed via x(); it is only
|
|
1132
|
+
# ever updated by its own transversal gate evolution and
|
|
1133
|
+
# _cpauli_lhv, same as every other branch here.
|
|
1134
|
+
|
|
1135
|
+
if (not skip_rotation) and (not end_caps_agree):
|
|
1136
|
+
a, i, rs = [0, 0, 0], [0, 0, 0], [0, 0, 0]
|
|
1137
|
+
a[0], i[0], rs[0] = self._get_bloch_angles(hq[0])
|
|
1138
|
+
a[1], i[1], rs[1] = self._get_bloch_angles(hq[1])
|
|
1139
|
+
a[2], i[2], rs[2] = self._get_bloch_angles(hq[2])
|
|
1140
|
+
w = [1 - r for r in rs]
|
|
1141
|
+
|
|
1142
|
+
w_total = sum(w)
|
|
1143
|
+
if w_total > self._epsilon:
|
|
1144
|
+
a_target = sum(wx * ax for wx, ax in zip(w, a)) / w_total
|
|
1145
|
+
i_target = sum(wx * ix for wx, ix in zip(w, i)) / w_total
|
|
1146
|
+
# slot0 (hq[0]) is excluded from the rotation itself,
|
|
1147
|
+
# same reasoning as every other branch here; slot1
|
|
1148
|
+
# and slot2 (the crossbar) are both real, lossy
|
|
1149
|
+
# shadow-style replicas with no comparable
|
|
1150
|
+
# entanglement to protect, so both are eligible,
|
|
1151
|
+
# each gated on its own separability.
|
|
1152
|
+
for x in (1, 2):
|
|
1153
|
+
if rs[x] <= self._rot_epsilon:
|
|
1154
|
+
self._rotate_to_bloch(hq[x], a_target - a[x], i_target - i[x])
|
|
1155
|
+
|
|
948
1156
|
else:
|
|
949
1157
|
# Edge-boundary qubit: only 2 real replicas remain (home +
|
|
950
1158
|
# adjacent-patch) now that the crossbar replica that used to
|
|
@@ -2112,36 +2320,21 @@ class QrackAceBackend:
|
|
|
2112
2320
|
return self.sim[b[0]].prob(b[1])
|
|
2113
2321
|
|
|
2114
2322
|
self._correct(lq)
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
qrms = math.sqrt(sum((1 - x) ** 2 for x in p) / 4)
|
|
2131
|
-
else:
|
|
2132
|
-
# The 2 real replicas are already mutually consistent here
|
|
2133
|
-
# (the _correct() call above already ran the conditional
|
|
2134
|
-
# tie-breaking cascade, using the LHV proxy as needed, and
|
|
2135
|
-
# forced agreement via x()). Re-weighting in the LHV here
|
|
2136
|
-
# would reintroduce the same center-dragging distortion the
|
|
2137
|
-
# cascade was built to avoid -- a plain RMS over the
|
|
2138
|
-
# now-settled pair is the correct, already-decided answer.
|
|
2139
|
-
p = [
|
|
2140
|
-
self.sim[hq[0][0]].prob(hq[0][1]),
|
|
2141
|
-
self.sim[hq[1][0]].prob(hq[1][1]),
|
|
2142
|
-
]
|
|
2143
|
-
prms = math.sqrt((p[0] ** 2 + p[1] ** 2) / 2)
|
|
2144
|
-
qrms = math.sqrt(((1 - p[0]) ** 2 + (1 - p[1]) ** 2) / 2)
|
|
2323
|
+
# The real replicas are already mutually consistent here (the
|
|
2324
|
+
# _correct() call above already ran the appropriate tie-breaking
|
|
2325
|
+
# cascade for however many replicas hq has -- 2, 3, 4, or 5,
|
|
2326
|
+
# depending on self.use_crossbars and whether this is an edge or
|
|
2327
|
+
# corner boundary qubit -- using the LHV proxy and/or the
|
|
2328
|
+
# crossbar replica as needed, and forced agreement via x()). A
|
|
2329
|
+
# plain, unweighted RMS over the now-settled replicas is the
|
|
2330
|
+
# correct, already-decided answer, regardless of replica count:
|
|
2331
|
+
# re-weighting any particular replica specially here would
|
|
2332
|
+
# reintroduce the same center-dragging distortion the cascade in
|
|
2333
|
+
# _correct() was built to avoid.
|
|
2334
|
+
p = [self.sim[b[0]].prob(b[1]) for b in hq]
|
|
2335
|
+
n = len(p)
|
|
2336
|
+
prms = math.sqrt(sum(x**2 for x in p) / n)
|
|
2337
|
+
qrms = math.sqrt(sum((1 - x) ** 2 for x in p) / n)
|
|
2145
2338
|
|
|
2146
2339
|
return (prms + (1 - qrms)) / 2
|
|
2147
2340
|
|
|
@@ -2523,7 +2716,7 @@ class QrackAceBackend:
|
|
|
2523
2716
|
self._classical_memory = 0
|
|
2524
2717
|
self._classical_register = 0
|
|
2525
2718
|
else:
|
|
2526
|
-
self._sim =
|
|
2719
|
+
self._sim = type(preamble_sim)(to_clone=preamble_sim)
|
|
2527
2720
|
self._classical_memory = preamble_memory
|
|
2528
2721
|
self._classical_register = preamble_register
|
|
2529
2722
|
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# (C) Daniel Strano and the Qrack contributors 2017-2025. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Use of this source code is governed by an MIT-style license that can be
|
|
4
|
+
# found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
5
|
+
#
|
|
6
|
+
# Produced with input from (Anthropic) Claude
|
|
7
|
+
#
|
|
8
|
+
# QrackAceBackend over matrix-product-state patches.
|
|
9
|
+
#
|
|
10
|
+
# QrackAceBackend's elision scheme (replicated boundary qubits, shadow
|
|
11
|
+
# couplers, _correct() reconciliation, LHV tie-breakers, error-detection
|
|
12
|
+
# gadgets) never inspects HOW a patch stores its state: it only drives each
|
|
13
|
+
# patch through a small "just-in-time state machine" interface -- single-
|
|
14
|
+
# qubit gates, (anti-)controlled Paulis, swap, prob(), m(), force_m(),
|
|
15
|
+
# clone(). QrackMPSPatch implements exactly that interface on an MPS, so
|
|
16
|
+
# every line of ACE's own logic runs unchanged on top of it.
|
|
17
|
+
#
|
|
18
|
+
# What MPS buys: a patch's cost is set by its entanglement (bond dimension
|
|
19
|
+
# chi), not by 2^(patch width), so patches far wider than a state vector
|
|
20
|
+
# could hold become possible when in-patch entanglement allows -- and, with
|
|
21
|
+
# max_bond/cutoff, a second, tunable approximation layer (truncation INSIDE
|
|
22
|
+
# a patch) composes with ACE's first one (elision BETWEEN patches).
|
|
23
|
+
#
|
|
24
|
+
# Implementation notes:
|
|
25
|
+
# * Plain numpy MPS, site tensors A[s] of shape (Dl, 2, Dr), with an
|
|
26
|
+
# explicitly tracked orthogonality center. (A quimb-backed prototype
|
|
27
|
+
# was ~180x slower inside ACE, almost entirely from re-deriving the
|
|
28
|
+
# orthogonality center on every prob() call -- and ACE calls prob()
|
|
29
|
+
# constantly. Tracking it explicitly makes prob() O(chi^2) plus the
|
|
30
|
+
# cost of moving the center, usually zero or one QR step.)
|
|
31
|
+
# * Logical qubits map to MPS sites through a permutation, so swap() is a
|
|
32
|
+
# free, exact relabeling -- the same reason ACE's own native swap is
|
|
33
|
+
# exact -- and non-adjacent 2-qubit gates are routed by adjacent SWAPs
|
|
34
|
+
# that are LEFT in place (relabeled), not undone.
|
|
35
|
+
# * Truncation: max_bond caps chi; cutoff is a per-split budget on
|
|
36
|
+
# discarded probability weight (sum of discarded s^2 / total s^2).
|
|
37
|
+
# set_sdrp() sets cutoff, as the MPS analog of Qrack's SDRP.
|
|
38
|
+
|
|
39
|
+
import math
|
|
40
|
+
import random
|
|
41
|
+
|
|
42
|
+
_IS_NUMPY_AVAILABLE = True
|
|
43
|
+
try:
|
|
44
|
+
import numpy as np
|
|
45
|
+
except:
|
|
46
|
+
_IS_NUMPY_AVAILABLE = False
|
|
47
|
+
|
|
48
|
+
from .pauli import Pauli
|
|
49
|
+
from .qrack_ace_backend import QrackAceBackend
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_I2 = np.eye(2, dtype=complex)
|
|
53
|
+
_X = np.array([[0, 1], [1, 0]], dtype=complex)
|
|
54
|
+
_Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
|
|
55
|
+
_Z = np.array([[1, 0], [0, -1]], dtype=complex)
|
|
56
|
+
_H = np.array([[1, 1], [1, -1]], dtype=complex) / math.sqrt(2)
|
|
57
|
+
_S = np.diag([1, 1j]).astype(complex)
|
|
58
|
+
_SDG = _S.conj().T
|
|
59
|
+
_T = np.diag([1, np.exp(1j * math.pi / 4)]).astype(complex)
|
|
60
|
+
_TDG = _T.conj().T
|
|
61
|
+
_SX = 0.5 * np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]], dtype=complex)
|
|
62
|
+
_SXDG = _SX.conj().T
|
|
63
|
+
_P0 = np.diag([1, 0]).astype(complex)
|
|
64
|
+
_P1 = np.diag([0, 1]).astype(complex)
|
|
65
|
+
_SWAP = np.array(
|
|
66
|
+
[[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dtype=complex
|
|
67
|
+
)
|
|
68
|
+
_PAULI_MTRX = {Pauli.PauliX: _X, Pauli.PauliY: _Y, Pauli.PauliZ: _Z}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _controlled(u, anti=False):
|
|
72
|
+
# 4x4 in the (control, target) basis, control = first (most significant).
|
|
73
|
+
g = np.eye(4, dtype=complex)
|
|
74
|
+
if anti:
|
|
75
|
+
g[0:2, 0:2] = u
|
|
76
|
+
else:
|
|
77
|
+
g[2:4, 2:4] = u
|
|
78
|
+
return g
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_CX = _controlled(_X)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class QrackMPSPatch:
|
|
85
|
+
"""One ACE patch held as an MPS, exposing the subset of the
|
|
86
|
+
QrackSimulator interface that QrackAceBackend calls on its patches.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
qubit_count: patch width.
|
|
90
|
+
max_bond: bond-dimension cap (None = no cap).
|
|
91
|
+
cutoff: per-split discarded-weight budget (0 = exact up to
|
|
92
|
+
numerical zeros).
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
_ZERO_TOL = 1e-14
|
|
96
|
+
|
|
97
|
+
def __init__(self, qubit_count, max_bond=None, cutoff=0.0, _state=None):
|
|
98
|
+
if not _IS_NUMPY_AVAILABLE:
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
"Before instantiating QrackAceMPSPatch, you must install numpy!"
|
|
101
|
+
)
|
|
102
|
+
self.n = qubit_count
|
|
103
|
+
self.max_bond = max_bond
|
|
104
|
+
self.cutoff = float(cutoff)
|
|
105
|
+
self.truncation_error = 0.0 # accumulated discarded weight
|
|
106
|
+
if _state is not None:
|
|
107
|
+
self.A, self.site_of, self.q_at, self.center = _state
|
|
108
|
+
else:
|
|
109
|
+
t = np.zeros((1, 2, 1), dtype=complex)
|
|
110
|
+
t[0, 0, 0] = 1.0
|
|
111
|
+
self.A = [t.copy() for _ in range(max(1, qubit_count))]
|
|
112
|
+
self.site_of = list(range(qubit_count))
|
|
113
|
+
self.q_at = list(range(qubit_count))
|
|
114
|
+
self.center = 0
|
|
115
|
+
|
|
116
|
+
# --- bookkeeping -------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def clone(self):
|
|
119
|
+
c = QrackMPSPatch(
|
|
120
|
+
self.n, self.max_bond, self.cutoff,
|
|
121
|
+
_state=([a.copy() for a in self.A], list(self.site_of), list(self.q_at), self.center),
|
|
122
|
+
)
|
|
123
|
+
c.truncation_error = self.truncation_error
|
|
124
|
+
return c
|
|
125
|
+
|
|
126
|
+
def set_sdrp(self, sdrp):
|
|
127
|
+
self.cutoff = max(0.0, float(sdrp))
|
|
128
|
+
|
|
129
|
+
def set_device(self, device_id):
|
|
130
|
+
# MPS patches run on the host; accepted for interface compatibility.
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def num_qubits(self):
|
|
134
|
+
return self.n
|
|
135
|
+
|
|
136
|
+
def max_bond_dim(self):
|
|
137
|
+
return max(a.shape[2] for a in self.A)
|
|
138
|
+
|
|
139
|
+
# --- canonical-form machinery -------------------------------------------
|
|
140
|
+
|
|
141
|
+
def _move_center(self, s):
|
|
142
|
+
A = self.A
|
|
143
|
+
while self.center < s:
|
|
144
|
+
c = self.center
|
|
145
|
+
dl, d, dr = A[c].shape
|
|
146
|
+
q, r = np.linalg.qr(A[c].reshape(dl * d, dr))
|
|
147
|
+
A[c] = q.reshape(dl, d, q.shape[1])
|
|
148
|
+
A[c + 1] = np.tensordot(r, A[c + 1], axes=(1, 0))
|
|
149
|
+
self.center += 1
|
|
150
|
+
while self.center > s:
|
|
151
|
+
c = self.center
|
|
152
|
+
dl, d, dr = A[c].shape
|
|
153
|
+
q, r = np.linalg.qr(A[c].reshape(dl, d * dr).T)
|
|
154
|
+
A[c] = q.T.reshape(q.shape[1], d, dr)
|
|
155
|
+
A[c - 1] = np.tensordot(A[c - 1], r.T, axes=(2, 0))
|
|
156
|
+
self.center -= 1
|
|
157
|
+
|
|
158
|
+
def _apply_site_1q(self, mat, s, unitary=True):
|
|
159
|
+
if not unitary:
|
|
160
|
+
# Non-unitary (projector): only safe at the orthogonality center.
|
|
161
|
+
self._move_center(s)
|
|
162
|
+
self.A[s] = np.einsum("ab,lbr->lar", mat, self.A[s])
|
|
163
|
+
|
|
164
|
+
def _apply_adjacent_2q(self, g4, s):
|
|
165
|
+
# g4 acts on (site s, site s+1), site s = most significant.
|
|
166
|
+
self._move_center(s)
|
|
167
|
+
a, b = self.A[s], self.A[s + 1]
|
|
168
|
+
dl, dr = a.shape[0], b.shape[2]
|
|
169
|
+
theta = np.tensordot(a, b, axes=(2, 0)) # (dl,2,2,dr)
|
|
170
|
+
theta = np.einsum("ijkl,aklb->aijb", g4.reshape(2, 2, 2, 2), theta)
|
|
171
|
+
u, sv, vh = np.linalg.svd(theta.reshape(dl * 2, 2 * dr), full_matrices=False)
|
|
172
|
+
w = sv ** 2
|
|
173
|
+
total = float(np.sum(w))
|
|
174
|
+
keep = len(sv)
|
|
175
|
+
# Drop numerical zeros always; drop more only within the budget.
|
|
176
|
+
tail = np.cumsum(w[::-1])[::-1] # tail[k] = sum_{j>=k} w[j]
|
|
177
|
+
budget = max(self.cutoff * total, self._ZERO_TOL * total)
|
|
178
|
+
while keep > 1 and tail[keep - 1] <= budget:
|
|
179
|
+
keep -= 1
|
|
180
|
+
if self.max_bond is not None and keep > self.max_bond:
|
|
181
|
+
keep = self.max_bond
|
|
182
|
+
discarded = float(np.sum(w[keep:]))
|
|
183
|
+
if discarded > 0 and total > 0:
|
|
184
|
+
self.truncation_error += discarded / total
|
|
185
|
+
u, sv, vh = u[:, :keep], sv[:keep], vh[:keep, :]
|
|
186
|
+
if discarded > 0:
|
|
187
|
+
# Restore the pre-truncation norm (the center carries it all).
|
|
188
|
+
sv = sv * math.sqrt(total / float(np.sum(sv ** 2)))
|
|
189
|
+
self.A[s] = u.reshape(dl, 2, keep)
|
|
190
|
+
self.A[s + 1] = (sv[:, None] * vh).reshape(keep, 2, dr)
|
|
191
|
+
self.center = s + 1
|
|
192
|
+
|
|
193
|
+
def _swap_sites(self, s):
|
|
194
|
+
# Physically exchange sites s, s+1 and relabel.
|
|
195
|
+
self._apply_adjacent_2q(_SWAP, s)
|
|
196
|
+
qa, qb = self.q_at[s], self.q_at[s + 1]
|
|
197
|
+
self.q_at[s], self.q_at[s + 1] = qb, qa
|
|
198
|
+
self.site_of[qa], self.site_of[qb] = s + 1, s
|
|
199
|
+
|
|
200
|
+
# --- gate application --------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def _g1(self, mat, q, unitary=True):
|
|
203
|
+
self._apply_site_1q(mat, self.site_of[q], unitary)
|
|
204
|
+
|
|
205
|
+
def _g2(self, g4, q1, q2):
|
|
206
|
+
# g4 acts on (q1, q2), q1 = most significant.
|
|
207
|
+
if q1 == q2:
|
|
208
|
+
raise ValueError("Two-qubit gate on identical qubits.")
|
|
209
|
+
# Route q2 next to q1 with adjacent swaps, left in place.
|
|
210
|
+
while abs(self.site_of[q1] - self.site_of[q2]) > 1:
|
|
211
|
+
s1, s2 = self.site_of[q1], self.site_of[q2]
|
|
212
|
+
if s2 > s1:
|
|
213
|
+
self._swap_sites(s2 - 1)
|
|
214
|
+
else:
|
|
215
|
+
self._swap_sites(s2)
|
|
216
|
+
s1, s2 = self.site_of[q1], self.site_of[q2]
|
|
217
|
+
if s1 < s2:
|
|
218
|
+
self._apply_adjacent_2q(g4, s1)
|
|
219
|
+
else:
|
|
220
|
+
self._apply_adjacent_2q(_SWAP @ g4 @ _SWAP, s2)
|
|
221
|
+
|
|
222
|
+
def mtrx(self, m, q):
|
|
223
|
+
self._g1(np.array([[m[0], m[1]], [m[2], m[3]]], dtype=complex), q)
|
|
224
|
+
|
|
225
|
+
def u(self, q, th, ph, la):
|
|
226
|
+
c, s = math.cos(th / 2), math.sin(th / 2)
|
|
227
|
+
self._g1(np.array(
|
|
228
|
+
[[c, -np.exp(1j * la) * s],
|
|
229
|
+
[np.exp(1j * ph) * s, np.exp(1j * (ph + la)) * c]], dtype=complex), q)
|
|
230
|
+
|
|
231
|
+
def r(self, b, ph, q):
|
|
232
|
+
p = _PAULI_MTRX.get(b)
|
|
233
|
+
if p is None:
|
|
234
|
+
return # PauliI: global phase only
|
|
235
|
+
self._g1(math.cos(ph / 2) * _I2 - 1j * math.sin(ph / 2) * p, q)
|
|
236
|
+
|
|
237
|
+
def h(self, q): self._g1(_H, q)
|
|
238
|
+
def x(self, q): self._g1(_X, q)
|
|
239
|
+
def y(self, q): self._g1(_Y, q)
|
|
240
|
+
def z(self, q): self._g1(_Z, q)
|
|
241
|
+
def s(self, q): self._g1(_S, q)
|
|
242
|
+
def adjs(self, q): self._g1(_SDG, q)
|
|
243
|
+
def t(self, q): self._g1(_T, q)
|
|
244
|
+
def adjt(self, q): self._g1(_TDG, q)
|
|
245
|
+
def sx(self, q): self._g1(_SX, q)
|
|
246
|
+
def adjsx(self, q): self._g1(_SXDG, q)
|
|
247
|
+
|
|
248
|
+
def _toffoli(self, c1, c2, t):
|
|
249
|
+
self.h(t)
|
|
250
|
+
self._g2(_CX, c2, t); self.adjt(t)
|
|
251
|
+
self._g2(_CX, c1, t); self.t(t)
|
|
252
|
+
self._g2(_CX, c2, t); self.adjt(t)
|
|
253
|
+
self._g2(_CX, c1, t); self.t(t)
|
|
254
|
+
self.h(t)
|
|
255
|
+
self.t(c2)
|
|
256
|
+
self._g2(_CX, c1, c2); self.t(c1); self.adjt(c2)
|
|
257
|
+
self._g2(_CX, c1, c2)
|
|
258
|
+
|
|
259
|
+
def _mc_pauli(self, c, q, u, anti):
|
|
260
|
+
c = list(c)
|
|
261
|
+
if len(c) == 0:
|
|
262
|
+
self._g1(u, q)
|
|
263
|
+
elif len(c) == 1:
|
|
264
|
+
self._g2(_controlled(u, anti), c[0], q)
|
|
265
|
+
elif len(c) == 2:
|
|
266
|
+
# Exact 1-/2-qubit Toffoli decomposition, conjugated into
|
|
267
|
+
# X, Y, or Z on the target.
|
|
268
|
+
if anti:
|
|
269
|
+
for x in c:
|
|
270
|
+
self.x(x)
|
|
271
|
+
pre, post = None, None
|
|
272
|
+
if u is _Y:
|
|
273
|
+
pre, post = _SDG, _S
|
|
274
|
+
elif u is _Z:
|
|
275
|
+
pre, post = _H, _H
|
|
276
|
+
if pre is not None:
|
|
277
|
+
self._g1(pre, q)
|
|
278
|
+
self._toffoli(c[0], c[1], q)
|
|
279
|
+
if post is not None:
|
|
280
|
+
self._g1(post, q)
|
|
281
|
+
if anti:
|
|
282
|
+
for x in c:
|
|
283
|
+
self.x(x)
|
|
284
|
+
else:
|
|
285
|
+
raise NotImplementedError("QrackMPSPatch supports at most 2 controls per gate.")
|
|
286
|
+
|
|
287
|
+
def mcx(self, c, q): self._mc_pauli(c, q, _X, False)
|
|
288
|
+
def mcy(self, c, q): self._mc_pauli(c, q, _Y, False)
|
|
289
|
+
def mcz(self, c, q): self._mc_pauli(c, q, _Z, False)
|
|
290
|
+
def macx(self, c, q): self._mc_pauli(c, q, _X, True)
|
|
291
|
+
def macy(self, c, q): self._mc_pauli(c, q, _Y, True)
|
|
292
|
+
def macz(self, c, q): self._mc_pauli(c, q, _Z, True)
|
|
293
|
+
|
|
294
|
+
def swap(self, q1, q2):
|
|
295
|
+
# Exact and free: relabel which site each logical qubit lives on.
|
|
296
|
+
if q1 == q2:
|
|
297
|
+
return
|
|
298
|
+
s1, s2 = self.site_of[q1], self.site_of[q2]
|
|
299
|
+
self.site_of[q1], self.site_of[q2] = s2, s1
|
|
300
|
+
self.q_at[s1], self.q_at[s2] = q2, q1
|
|
301
|
+
|
|
302
|
+
def cswap(self, c, q1, q2):
|
|
303
|
+
c = list(c)
|
|
304
|
+
if q1 == q2:
|
|
305
|
+
return
|
|
306
|
+
if len(c) != 1:
|
|
307
|
+
raise NotImplementedError("QrackMPSPatch.cswap() supports exactly 1 control.")
|
|
308
|
+
self._g2(_CX, q2, q1)
|
|
309
|
+
self._toffoli(c[0], q1, q2)
|
|
310
|
+
self._g2(_CX, q2, q1)
|
|
311
|
+
|
|
312
|
+
# --- readout -----------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
def prob(self, q):
|
|
315
|
+
s = self.site_of[q]
|
|
316
|
+
self._move_center(s)
|
|
317
|
+
a = self.A[s]
|
|
318
|
+
total = float(np.sum(np.abs(a) ** 2))
|
|
319
|
+
if total <= 0:
|
|
320
|
+
return 0.0
|
|
321
|
+
one = float(np.sum(np.abs(a[:, 1, :]) ** 2))
|
|
322
|
+
return min(1.0, max(0.0, one / total))
|
|
323
|
+
|
|
324
|
+
def force_m(self, q, r):
|
|
325
|
+
s = self.site_of[q]
|
|
326
|
+
self._move_center(s)
|
|
327
|
+
a = self.A[s].copy()
|
|
328
|
+
a[:, 0 if r else 1, :] = 0
|
|
329
|
+
nrm = math.sqrt(float(np.sum(np.abs(a) ** 2)))
|
|
330
|
+
if nrm > 0:
|
|
331
|
+
self.A[s] = a / nrm
|
|
332
|
+
return bool(r)
|
|
333
|
+
|
|
334
|
+
def m(self, q):
|
|
335
|
+
return self.force_m(q, random.random() < self.prob(q))
|
|
336
|
+
|
|
337
|
+
def m_all(self):
|
|
338
|
+
result = 0
|
|
339
|
+
for q in range(self.n):
|
|
340
|
+
if self.m(q):
|
|
341
|
+
result |= 1 << q
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
# --- dense readout (testing only; 2^n) ----------------------------------
|
|
345
|
+
|
|
346
|
+
def out_ket(self):
|
|
347
|
+
v = self.A[0]
|
|
348
|
+
for a in self.A[1:]:
|
|
349
|
+
v = np.tensordot(v, a, axes=(v.ndim - 1, 0))
|
|
350
|
+
v = v.reshape([2] * self.n) # axis k = site k
|
|
351
|
+
# Reorder axes into Qrack's little-endian logical order: the
|
|
352
|
+
# C-order flatten treats axis 0 as most significant, so axis 0
|
|
353
|
+
# must be logical qubit n-1.
|
|
354
|
+
v = np.transpose(v, [self.site_of[q] for q in range(self.n - 1, -1, -1)])
|
|
355
|
+
return v.reshape(-1)
|
|
356
|
+
|
|
357
|
+
def out_probs(self):
|
|
358
|
+
return np.abs(self.out_ket()) ** 2
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class QrackAceMPSBackend(QrackAceBackend):
|
|
362
|
+
"""QrackAceBackend whose patches are MPS (QrackMPSPatch) instead of
|
|
363
|
+
QrackSimulator. All elision, shadow-coupling, reconciliation and
|
|
364
|
+
error-detection logic is inherited unchanged.
|
|
365
|
+
|
|
366
|
+
Extra args:
|
|
367
|
+
max_bond: per-patch MPS bond-dimension cap (None = no cap).
|
|
368
|
+
mps_cutoff: per-split discarded-weight budget (0 = exact).
|
|
369
|
+
QrackSimulator-only construction options (is_gpu, is_stabilizer_hybrid,
|
|
370
|
+
...) are accepted and ignored.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
def __init__(self, *args, max_bond=None, mps_cutoff=0.0, to_clone=None, **kwargs):
|
|
374
|
+
if not _IS_NUMPY_AVAILABLE:
|
|
375
|
+
raise RuntimeError(
|
|
376
|
+
"Before instantiating QrackAceMPSBackend, you must install numpy!"
|
|
377
|
+
)
|
|
378
|
+
if to_clone is not None:
|
|
379
|
+
max_bond = to_clone.max_bond
|
|
380
|
+
mps_cutoff = to_clone.mps_cutoff
|
|
381
|
+
self.max_bond = max_bond
|
|
382
|
+
self.mps_cutoff = mps_cutoff
|
|
383
|
+
super().__init__(*args, to_clone=to_clone, **kwargs)
|
|
384
|
+
|
|
385
|
+
def _new_patch_sim(self, qubit_count, sim_kwargs):
|
|
386
|
+
return QrackMPSPatch(qubit_count, max_bond=self.max_bond, cutoff=self.mps_cutoff)
|
|
387
|
+
|
|
388
|
+
def max_bond_dims(self):
|
|
389
|
+
return [s.max_bond_dim() for s in self.sim]
|
|
390
|
+
|
|
391
|
+
def truncation_errors(self):
|
|
392
|
+
return [s.truncation_error for s in self.sim]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/neuron_activation_fn.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_neuron_torch_layer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_system/__init__.py
RENAMED
|
File without changes
|
{pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_system/qrack_system.py
RENAMED
|
File without changes
|
{pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/quimb_circuit_type.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/load_quantized_data.py
RENAMED
|
File without changes
|
{pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/quantize_by_range.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|