pyqrack-cpu-complex128 2.27.0__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.
Files changed (31) hide show
  1. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/Makefile +1 -1
  2. {pyqrack_cpu_complex128-2.27.0/pyqrack_cpu_complex128.egg-info → pyqrack_cpu_complex128-2.28.0}/PKG-INFO +1 -1
  3. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/__init__.py +1 -0
  4. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_ace_backend.py +21 -12
  5. pyqrack_cpu_complex128-2.28.0/pyqrack/qrack_ace_mps_backend.py +392 -0
  6. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0/pyqrack_cpu_complex128.egg-info}/PKG-INFO +1 -1
  7. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/SOURCES.txt +1 -0
  8. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/setup.py +1 -1
  9. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/LICENSE +0 -0
  10. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/MANIFEST.in +0 -0
  11. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/README.md +0 -0
  12. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyproject.toml +0 -0
  13. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/neuron_activation_fn.py +0 -0
  14. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/pauli.py +0 -0
  15. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_circuit.py +0 -0
  16. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  17. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_neuron.py +0 -0
  18. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  19. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_simulator.py +0 -0
  20. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_stabilizer.py +0 -0
  21. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_system/__init__.py +0 -0
  22. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_system/qrack_system.py +0 -0
  23. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/quimb_circuit_type.py +0 -0
  24. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/__init__.py +0 -0
  25. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/load_quantized_data.py +0 -0
  26. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/quantize_by_range.py +0 -0
  27. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/dependency_links.txt +0 -0
  28. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/not-zip-safe +0 -0
  29. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/requires.txt +0 -0
  30. {pyqrack_cpu_complex128-2.27.0 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/top_level.txt +0 -0
  31. {pyqrack_cpu_complex128-2.27.0 → 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 b140695ddde41ecd9099bad08e9cc0a32a46a9f9; cd ..
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu-complex128
3
- Version: 2.27.0
3
+ Version: 2.28.0
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -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
@@ -498,25 +498,34 @@ class QrackAceBackend:
498
498
  # "Golden value"
499
499
  self._sdrp = 0.0
500
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
+ )
501
510
  self.sim = []
502
511
  for i in range(sim_count):
503
512
  self.sim.append(
504
513
  to_clone.sim[i].clone()
505
514
  if to_clone
506
- else QrackSimulator(
507
- sim_counts[i],
508
- is_schmidt_decompose_multi=is_schmidt_decompose_multi,
509
- is_stabilizer_hybrid=is_stabilizer_hybrid,
510
- is_binary_decision_tree=is_binary_decision_tree,
511
- is_gpu=is_gpu,
512
- is_host_pointer=is_host_pointer,
513
- is_near_clifford_tableau_writer=is_near_clifford_tableau_writer,
514
- noise=noise,
515
- )
515
+ else self._new_patch_sim(sim_counts[i], sim_kwargs)
516
516
  )
517
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
+
518
525
  def clone(self):
519
- return QrackAceBackend(to_clone=self)
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)
520
529
 
521
530
  def set_sdrp(self, sdrp):
522
531
  sdrp = min(1, sdrp)
@@ -2707,7 +2716,7 @@ class QrackAceBackend:
2707
2716
  self._classical_memory = 0
2708
2717
  self._classical_register = 0
2709
2718
  else:
2710
- self._sim = QrackAceBackend(to_clone=preamble_sim)
2719
+ self._sim = type(preamble_sim)(to_clone=preamble_sim)
2711
2720
  self._classical_memory = preamble_memory
2712
2721
  self._classical_register = preamble_register
2713
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]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu-complex128
3
- Version: 2.27.0
3
+ Version: 2.28.0
4
4
  Summary: pyqrack - Pure Python vm6502q/qrack Wrapper
5
5
  Home-page: https://github.com/vm6502q/pyqrack
6
6
  Author: Daniel Strano
@@ -8,6 +8,7 @@ pyqrack/__init__.py
8
8
  pyqrack/neuron_activation_fn.py
9
9
  pyqrack/pauli.py
10
10
  pyqrack/qrack_ace_backend.py
11
+ pyqrack/qrack_ace_mps_backend.py
11
12
  pyqrack/qrack_circuit.py
12
13
  pyqrack/qrack_near_clifford_qec_backend.py
13
14
  pyqrack/qrack_neuron.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.27.0"
10
+ VERSION = "2.28.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')