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.
Files changed (31) hide show
  1. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/Makefile +1 -1
  2. {pyqrack_cpu_complex128-2.26.6/pyqrack_cpu_complex128.egg-info → pyqrack_cpu_complex128-2.28.0}/PKG-INFO +1 -1
  3. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/__init__.py +1 -0
  4. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_ace_backend.py +258 -65
  5. pyqrack_cpu_complex128-2.28.0/pyqrack/qrack_ace_mps_backend.py +392 -0
  6. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0/pyqrack_cpu_complex128.egg-info}/PKG-INFO +1 -1
  7. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/SOURCES.txt +1 -0
  8. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/setup.py +1 -1
  9. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/LICENSE +0 -0
  10. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/MANIFEST.in +0 -0
  11. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/README.md +0 -0
  12. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyproject.toml +0 -0
  13. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/neuron_activation_fn.py +0 -0
  14. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/pauli.py +0 -0
  15. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_circuit.py +0 -0
  16. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_near_clifford_qec_backend.py +0 -0
  17. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_neuron.py +0 -0
  18. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_neuron_torch_layer.py +0 -0
  19. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_simulator.py +0 -0
  20. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_stabilizer.py +0 -0
  21. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_system/__init__.py +0 -0
  22. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/qrack_system/qrack_system.py +0 -0
  23. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/quimb_circuit_type.py +0 -0
  24. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/__init__.py +0 -0
  25. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/load_quantized_data.py +0 -0
  26. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack/stats/quantize_by_range.py +0 -0
  27. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/dependency_links.txt +0 -0
  28. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/not-zip-safe +0 -0
  29. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/requires.txt +0 -0
  30. {pyqrack_cpu_complex128-2.26.6 → pyqrack_cpu_complex128-2.28.0}/pyqrack_cpu_complex128.egg-info/top_level.txt +0 -0
  31. {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 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.26.6
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
@@ -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 has been removed.
338
- # It used to give every boundary site (row- and/or column-boundary
339
- # alike) one extra real replica in a single simulator shared by ALL
340
- # boundary qubits, trusting that simulator's own greedy elision
341
- # (set_sdrp) to automatically factor apart whatever boundary sites
342
- # turned out to be separable, rather than hand-coding "crossbar
343
- # intersection" logic. Measurement showed it adds real, and
344
- # growing-with-patch-count, connectivity value -- but per explicit
345
- # instruction, it is being ripped out cleanly regardless, to get an
346
- # empirical read on XEB without it. Every boundary qubit already
347
- # carries a classical LHVQubit proxy (self._lhv, below) in
348
- # parallel; _correct() now falls back to that proxy for tie-
349
- # breaking wherever the crossbar replica used to be consulted.
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 QrackSimulator(
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
- 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)
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
- # With the shared boundary "crossbar" QrackSimulator removed,
733
- # every entry of hq is again an ordinary (sim_id, idx) tuple into a
734
- # real physical replica: 1 for a bulk qubit, 2 for an edge-boundary
735
- # qubit (home + adjacent-patch), or 4 for a corner-boundary qubit
736
- # (home + adjacent-patch + 2 diagonal-patch). None of them are
737
- # virtual/LHV-backed slots any more -- the classical LHVQubit
738
- # proxy (self._lhv) is tracked entirely separately from hq, not as
739
- # one of its indices -- so qb is simply every index, in order. We
740
- # keep this helper's name and signature for minimal call-site
741
- # churn; "lhv" is always -1 (no index needs special-casing).
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
- if len(hq) == 4:
2116
- # The 4 real replicas are already mutually consistent here
2117
- # (the _correct() call above already ran the tie-breaking
2118
- # cascade, using the LHV proxy as needed, and forced
2119
- # agreement via x()). A plain, unweighted RMS over the
2120
- # now-settled replicas is the correct, already-decided
2121
- # answer -- there is no crossbar replica left to weight
2122
- # specially any more.
2123
- p = [
2124
- self.sim[hq[0][0]].prob(hq[0][1]),
2125
- self.sim[hq[1][0]].prob(hq[1][1]),
2126
- self.sim[hq[2][0]].prob(hq[2][1]),
2127
- self.sim[hq[3][0]].prob(hq[3][1]),
2128
- ]
2129
- prms = math.sqrt(sum(x**2 for x in p) / 4)
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 = QrackAceBackend(to_clone=preamble_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]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyqrack-cpu-complex128
3
- Version: 2.26.6
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.26.6"
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')