dense-evolution 8.3.0__py3-none-win_amd64.whl

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 (165) hide show
  1. dashboard_core/__init__.py +115 -0
  2. dashboard_core/_gate_tables.py +30 -0
  3. dashboard_core/band_structure.py +71 -0
  4. dashboard_core/circuit_builder_component.py +232 -0
  5. dashboard_core/circuit_diagram.py +216 -0
  6. dashboard_core/crypto_protocols.py +77 -0
  7. dashboard_core/engine.py +326 -0
  8. dashboard_core/graphical_builder.py +114 -0
  9. dashboard_core/hamiltonians.py +593 -0
  10. dashboard_core/mass_decomposition_tool.py +47 -0
  11. dashboard_core/mitigation.py +343 -0
  12. dashboard_core/native_hf_diagnostics.py +62 -0
  13. dashboard_core/noise_tools.py +125 -0
  14. dashboard_core/qasm_library.py +233 -0
  15. dashboard_core/qmmm.py +16 -0
  16. dashboard_core/rag_tool.py +45 -0
  17. dashboard_core/state_visuals.py +288 -0
  18. dashboard_core/system_limits.py +60 -0
  19. dashboard_core/vector_healing.py +102 -0
  20. dashboard_core/visuals.py +158 -0
  21. dashboard_core/vqe.py +533 -0
  22. dashboard_core/wormhole.py +580 -0
  23. dense_evolution/__init__.py +114 -0
  24. dense_evolution/autodiff.py +10 -0
  25. dense_evolution/backends/__init__.py +5 -0
  26. dense_evolution/backends/chunk/__init__.py +37 -0
  27. dense_evolution/backends/chunk/_engine_imports.py +57 -0
  28. dense_evolution/backends/chunk/circuit_chunker.py +55 -0
  29. dense_evolution/backends/chunk/core.py +432 -0
  30. dense_evolution/backends/chunk/disk_overflow.py +232 -0
  31. dense_evolution/backends/chunk/geometry.py +95 -0
  32. dense_evolution/backends/chunk/guard.py +190 -0
  33. dense_evolution/backends/chunk/kernels.py +531 -0
  34. dense_evolution/backends/mps.py +1569 -0
  35. dense_evolution/backends/statevector.py +616 -0
  36. dense_evolution/chunk.py +25 -0
  37. dense_evolution/circuits/__init__.py +20 -0
  38. dense_evolution/circuits/compiler.py +488 -0
  39. dense_evolution/circuits/diagram.py +94 -0
  40. dense_evolution/circuits/gates.py +91 -0
  41. dense_evolution/circuits/parser.py +632 -0
  42. dense_evolution/circuits/qft.py +66 -0
  43. dense_evolution/circuits/random_circuit.py +85 -0
  44. dense_evolution/circuits/registry.py +74 -0
  45. dense_evolution/circuits/topology.py +79 -0
  46. dense_evolution/circuits/trotter.py +265 -0
  47. dense_evolution/circuits/uccsd.py +275 -0
  48. dense_evolution/cli.py +199 -0
  49. dense_evolution/compiler.py +9 -0
  50. dense_evolution/config.py +49 -0
  51. dense_evolution/drawing.py +10 -0
  52. dense_evolution/entropy.py +9 -0
  53. dense_evolution/fermions.py +9 -0
  54. dense_evolution/gates.py +9 -0
  55. dense_evolution/harrison_tb.py +16 -0
  56. dense_evolution/healing.py +18 -0
  57. dense_evolution/interop/__init__.py +18 -0
  58. dense_evolution/interop/qiskit_pennylane.py +406 -0
  59. dense_evolution/measurement.py +10 -0
  60. dense_evolution/mitigation/__init__.py +54 -0
  61. dense_evolution/mitigation/healing.py +215 -0
  62. dense_evolution/mitigation/kl_divergence.py +93 -0
  63. dense_evolution/mitigation/magic_entropy.py +163 -0
  64. dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
  65. dense_evolution/mitigation/renyi.py +168 -0
  66. dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
  67. dense_evolution/mitigation/zne.py +990 -0
  68. dense_evolution/mps.py +9 -0
  69. dense_evolution/native_hf/__init__.py +26 -0
  70. dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
  71. dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
  72. dense_evolution/native_hf/assembly.py +304 -0
  73. dense_evolution/native_hf/basis.py +117 -0
  74. dense_evolution/native_hf/boys.py +35 -0
  75. dense_evolution/native_hf/bridge.py +112 -0
  76. dense_evolution/native_hf/cartesian.py +64 -0
  77. dense_evolution/native_hf/coulomb.py +196 -0
  78. dense_evolution/native_hf/differentiable.py +53 -0
  79. dense_evolution/native_hf/gaussians.py +79 -0
  80. dense_evolution/native_hf/kinetic.py +52 -0
  81. dense_evolution/native_hf/libcint_bridge.py +167 -0
  82. dense_evolution/native_hf/overlap.py +91 -0
  83. dense_evolution/native_hf/scf.py +404 -0
  84. dense_evolution/noise/__init__.py +79 -0
  85. dense_evolution/noise/coherent_attack.py +264 -0
  86. dense_evolution/noise/cosmic_ray.py +61 -0
  87. dense_evolution/noise/density_matrix_channels.py +78 -0
  88. dense_evolution/noise/differentiable.py +66 -0
  89. dense_evolution/noise/kraus/__init__.py +6 -0
  90. dense_evolution/noise/kraus/amplitude_damping.py +47 -0
  91. dense_evolution/noise/kraus/bitflip.py +22 -0
  92. dense_evolution/noise/kraus/combined.py +16 -0
  93. dense_evolution/noise/kraus/depolarizing.py +47 -0
  94. dense_evolution/noise/kraus/ideal.py +10 -0
  95. dense_evolution/noise/kraus/phaseflip.py +21 -0
  96. dense_evolution/noise/kraus_channels.py +285 -0
  97. dense_evolution/noise/oscillating.py +32 -0
  98. dense_evolution/noise/pink.py +80 -0
  99. dense_evolution/observables.py +11 -0
  100. dense_evolution/parser.py +9 -0
  101. dense_evolution/physics/__init__.py +27 -0
  102. dense_evolution/physics/entropy.py +161 -0
  103. dense_evolution/physics/fermions.py +322 -0
  104. dense_evolution/physics/observables.py +523 -0
  105. dense_evolution/physics/qec.py +1113 -0
  106. dense_evolution/physics/spectral.py +143 -0
  107. dense_evolution/physics/states.py +43 -0
  108. dense_evolution/protocols/__init__.py +27 -0
  109. dense_evolution/protocols/bb84.py +133 -0
  110. dense_evolution/protocols/di_qkd_ghz.py +199 -0
  111. dense_evolution/protocols/dicka_protocol2.py +124 -0
  112. dense_evolution/qec.py +20 -0
  113. dense_evolution/qft.py +9 -0
  114. dense_evolution/qmmm/__init__.py +13 -0
  115. dense_evolution/qmmm/ase_bridge.py +97 -0
  116. dense_evolution/qmmm/forces.py +388 -0
  117. dense_evolution/qmmm/propagation.py +80 -0
  118. dense_evolution/qmmm/region.py +137 -0
  119. dense_evolution/random_circuit.py +15 -0
  120. dense_evolution/registry.py +9 -0
  121. dense_evolution/simulator.py +10 -0
  122. dense_evolution/solvers/__init__.py +19 -0
  123. dense_evolution/solvers/autodiff.py +169 -0
  124. dense_evolution/solvers/harrison_tb.py +189 -0
  125. dense_evolution/solvers/vhd_tb.py +187 -0
  126. dense_evolution/states.py +9 -0
  127. dense_evolution/topology.py +9 -0
  128. dense_evolution/trotter.py +9 -0
  129. dense_evolution/utils/__init__.py +13 -0
  130. dense_evolution/utils/drawing.py +101 -0
  131. dense_evolution/utils/mass_decomposition.py +246 -0
  132. dense_evolution/utils/measurement.py +94 -0
  133. dense_evolution/vhd_tb.py +16 -0
  134. dense_evolution-8.3.0.dist-info/METADATA +366 -0
  135. dense_evolution-8.3.0.dist-info/RECORD +165 -0
  136. dense_evolution-8.3.0.dist-info/WHEEL +5 -0
  137. dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
  138. dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
  139. dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
  140. ia_utils/__init__.py +0 -0
  141. ia_utils/adversarial_vector_attack.py +196 -0
  142. ia_utils/rag.py +288 -0
  143. ia_utils/vector_healing.py +399 -0
  144. local_site/__init__.py +0 -0
  145. local_site/app/__init__.py +0 -0
  146. local_site/app/server.py +1009 -0
  147. mcp_server/__init__.py +0 -0
  148. mcp_server/client.py +324 -0
  149. mcp_server/config.py +32 -0
  150. mcp_server/models.py +347 -0
  151. mcp_server/molecules.py +71 -0
  152. mcp_server/server.py +119 -0
  153. mcp_server/tools/__init__.py +0 -0
  154. mcp_server/tools/chemistry_tools.py +225 -0
  155. mcp_server/tools/circuit_tools.py +83 -0
  156. mcp_server/tools/crypto_tools.py +66 -0
  157. mcp_server/tools/mitigation_tools.py +81 -0
  158. mcp_server/tools/noise_tools.py +60 -0
  159. mcp_server/tools/retrieval_tools.py +44 -0
  160. mcp_server/tools/system_tools.py +149 -0
  161. mcp_server/tools/wormhole_tools.py +142 -0
  162. mcp_server/utils/__init__.py +0 -0
  163. mcp_server/utils/cache.py +55 -0
  164. mcp_server/utils/images.py +67 -0
  165. mcp_server/utils/truncation.py +38 -0
@@ -0,0 +1,187 @@
1
+ """
2
+ Vogl-Hjalmarson-Dow (1983) material-specific sp3s* tight-binding
3
+ parameters -- a more accurate alternative to harrison_tb's universal
4
+ (materials-independent) parameters, at the cost of needing a separately
5
+ fitted parameter set per material.
6
+
7
+ Source: P. Vogl, H. P. Hjalmarson, J. D. Dow, "A Semi-empirical
8
+ tight-binding theory of the electronic structure of semiconductors",
9
+ J. Phys. Chem. Solids 44 (5), 365-378 (1983). Parameter values below are
10
+ transcribed from an independent open-source implementation of the same
11
+ table, github.com/rpmuller/TightBinding (`TB.py`, fetched 2026-08-05),
12
+ which cites the same paper; not reproduced from the paper itself (not
13
+ open access). Only materials appearing in that source are included.
14
+
15
+ sp3s* basis: s, px, py, pz, and an extra excited s* orbital per atom
16
+ (5 orbitals/atom, 10x10 Hamiltonian per zinc-blende/diamond 2-atom
17
+ cell) -- the s* orbital exists purely to push the lowest conduction
18
+ band down to the right energy without needing d-orbitals; it has no
19
+ literal physical excited state behind it.
20
+
21
+ Hermiticity note: the reference implementation's own H-builder forms
22
+ the lower-left (cation-anion) block as `Hac.conjugate()` without
23
+ transposing it, which does not in general produce a Hermitian matrix
24
+ (the block mixes Vsapc/Vscpa asymmetrically). `_cation_anion_block`
25
+ below is built the same way as that source's `get_Hac`, but assembled
26
+ into the full Hamiltonian with a proper conjugate *transpose*
27
+ (`.conj().T`) -- verified Hermitian for every material in `MATERIALS`
28
+ (see the module's own smoke test).
29
+
30
+ Validated against experiment (run 2026-08-05): GaAs direct gap at
31
+ Gamma (k=0) computes to 1.55 eV vs. the experimental 1.42 eV (~9%
32
+ high) -- compare to harrison_tb.zincblende_hamiltonian's 2.91 eV
33
+ (~105% high) for the same material using Harrison's universal
34
+ parameters. The improvement is expected: these parameters are fitted
35
+ per material specifically to reproduce band-edge energies, unlike
36
+ Harrison's one-table-fits-all approach.
37
+ """
38
+ import numpy as np
39
+ from collections import namedtuple
40
+
41
+ __all__ = ['Material', 'MATERIALS', 'sp3s_star_hamiltonian', 'direct_gap_at_gamma',
42
+ 'band_extrema_along_path']
43
+
44
+ Material = namedtuple('Material', [
45
+ 'name', 'Esa', 'Epa', 'Esc', 'Epc', 'Essa', 'Essc',
46
+ 'Vss', 'Vxx', 'Vxy', 'Vsapc', 'Vscpa', 'Vssapc', 'Vsscpa',
47
+ ])
48
+
49
+ # name: (Esa, Epa, Esc, Epc, Essa, Essc, Vss, Vxx, Vxy, Vsapc, Vscpa, Vssapc, Vsscpa)
50
+ # 'a' = anion, 'c' = cation. All values in eV.
51
+ MATERIALS = {
52
+ 'C': Material('C', -4.5450, 3.8400, -4.5450, 3.8400, 11.3700, 11.3700,
53
+ -22.7250, 3.8400, 11.6700, 15.2206, 15.2206, 8.2109, 8.2109),
54
+ 'Si': Material('Si', -4.2000, 1.7150, -4.2000, 1.7150, 6.6850, 6.6850,
55
+ -8.3000, 1.7150, 4.5750, 5.7292, 5.7292, 5.3749, 5.3749),
56
+ 'Ge': Material('Ge', -5.8800, 1.6100, -5.8800, 1.6100, 6.3900, 6.3900,
57
+ -6.7800, 1.6100, 4.9000, 5.4649, 5.4649, 5.2191, 5.2191),
58
+ 'Sn': Material('Sn', -5.6700, 1.3300, -5.6700, 1.3300, 5.9000, 5.9000,
59
+ -5.6700, 1.3300, 4.0800, 4.5116, 4.5116, 5.8939, 5.8939),
60
+ 'SiC': Material('SiC', -8.4537, 2.1234, -4.8463, 4.3466, 9.6534, 9.3166,
61
+ -12.4197, 3.0380, 5.9216, 9.4900, 9.2007, 8.7138, 4.4051),
62
+ 'AlP': Material('AlP', -7.8466, 1.3169, -1.2534, 4.2831, 8.7069, 7.4231,
63
+ -7.4535, 2.3749, 4.8378, 5.2451, 5.2775, 5.2508, 4.6388),
64
+ 'AlAs': Material('AlAs', -7.5273, 0.9833, -1.1627, 3.5867, 7.4833, 6.7267,
65
+ -6.6642, 1.8780, 4.2919, 5.1106, 5.4965, 4.5216, 4.9950),
66
+ 'AlSb': Material('AlSb', -6.1714, 0.9807, -2.0716, 3.0163, 6.7607, 6.1543,
67
+ -5.6448, 1.7199, 3.6648, 4.9121, 4.2137, 4.3662, 3.0739),
68
+ 'GaP': Material('GaP', -8.1124, 1.1250, -2.1976, 4.1150, 8.5150, 7.1850,
69
+ -7.4709, 2.1516, 5.1369, 4.2771, 6.3190, 4.6541, 5.0950),
70
+ 'GaAs': Material('GaAs', -8.3431, 1.0414, -2.6569, 3.6686, 8.5914, 6.7386,
71
+ -6.4513, 1.9546, 5.0779, 4.4800, 5.7839, 4.8422, 4.8077),
72
+ 'GaSb': Material('GaSb', -7.3207, 0.8554, -3.8993, 2.9146, 6.6354, 5.9846,
73
+ -6.1567, 1.5789, 4.1285, 4.9601, 4.6675, 4.9895, 4.2180),
74
+ 'InP': Material('InP', -8.5274, 0.8735, -1.4826, 4.0465, 8.2635, 7.0665,
75
+ -5.3614, 1.8801, 4.2324, 2.2265, 5.5825, 3.4623, 4.4814),
76
+ 'InAs': Material('InAs', -9.5381, 0.9099, -2.7219, 3.7201, 7.4099, 6.7401,
77
+ -5.6052, 1.8398, 4.4693, 3.0354, 5.4389, 3.3744, 3.9097),
78
+ 'InSb': Material('InSb', -8.0157, 0.6738, -3.4643, 2.9162, 6.4530, 5.9362,
79
+ -5.5193, 1.4018, 3.8761, 3.7880, 4.5900, 3.5666, 3.4048),
80
+ 'ZnSe': Material('ZnSe', -11.8383, 1.5072, 0.0183, 5.9928, 7.5872, 8.9928,
81
+ -6.2163, 3.0054, 5.9942, 3.4980, 6.3191, 2.5891, 3.9533),
82
+ 'ZnTe': Material('ZnTe', -9.8150, 1.4834, 0.9350, 5.2666, 7.0834, 8.2666,
83
+ -6.5765, 2.7951, 5.4670, 5.9827, 5.8199, 1.3196, 0.0000),
84
+ }
85
+
86
+
87
+ def _phase_factors(kxyz):
88
+ """g0..g3 structure factors for the 4 zinc-blende nearest-neighbor
89
+ bonds, for k given in units of 2*pi/a along the conventional cubic
90
+ axes (so Gamma=(0,0,0), X=(1,0,0), L=(0.5,0.5,0.5))."""
91
+ kxp, kyp, kzp = (np.pi / 2) * np.asarray(kxyz, dtype=float)
92
+ g0 = np.cos(kxp) * np.cos(kyp) * np.cos(kzp) - 1j * np.sin(kxp) * np.sin(kyp) * np.sin(kzp)
93
+ g1 = -np.cos(kxp) * np.sin(kyp) * np.sin(kzp) + 1j * np.sin(kxp) * np.cos(kyp) * np.cos(kzp)
94
+ g2 = -np.sin(kxp) * np.cos(kyp) * np.sin(kzp) + 1j * np.cos(kxp) * np.sin(kyp) * np.cos(kzp)
95
+ g3 = -np.sin(kxp) * np.sin(kyp) * np.cos(kzp) + 1j * np.cos(kxp) * np.cos(kyp) * np.sin(kzp)
96
+ return g0, g1, g2, g3
97
+
98
+
99
+ def _cation_anion_block(mat, kxyz):
100
+ """5x5 anion-row x cation-column hopping block (s,px,py,pz,s* basis
101
+ on each side), before Hermitian assembly."""
102
+ g0, g1, g2, g3 = _phase_factors(kxyz)
103
+ return np.array([
104
+ [mat.Vss * g0, mat.Vscpa * g1, mat.Vscpa * g2, mat.Vscpa * g3, 0],
105
+ [-mat.Vsapc * g1, mat.Vxx * g0, mat.Vxy * g3, mat.Vxy * g2, -mat.Vssapc * g1],
106
+ [-mat.Vsapc * g2, mat.Vxy * g3, mat.Vxx * g0, mat.Vxy * g1, -mat.Vssapc * g2],
107
+ [-mat.Vsapc * g3, mat.Vxy * g2, mat.Vxy * g1, mat.Vxx * g0, -mat.Vssapc * g3],
108
+ [0, mat.Vsscpa * g1, mat.Vsscpa * g2, mat.Vsscpa * g3, 0],
109
+ ], dtype=np.complex128)
110
+
111
+
112
+ def sp3s_star_hamiltonian(kxyz, material):
113
+ """
114
+ 10x10 sp3s* Bloch Hamiltonian, basis order
115
+ [anion: s,px,py,pz,s*, cation: s,px,py,pz,s*].
116
+
117
+ material: a Material namedtuple, or a key into MATERIALS.
118
+ kxyz: crystal momentum in units of 2*pi/a along the conventional
119
+ cubic axes (Gamma=(0,0,0), X=(1,0,0), L=(0.5,0.5,0.5)).
120
+ """
121
+ if isinstance(material, str):
122
+ if material not in MATERIALS:
123
+ raise ValueError(f"no VHD parameters for {material!r}; available: {sorted(MATERIALS)}")
124
+ material = MATERIALS[material]
125
+
126
+ Ha = np.diag([material.Esa, material.Epa, material.Epa, material.Epa, material.Essa])
127
+ Hc = np.diag([material.Esc, material.Epc, material.Epc, material.Epc, material.Essc])
128
+ Hac = _cation_anion_block(material, kxyz)
129
+
130
+ H = np.zeros((10, 10), dtype=np.complex128)
131
+ H[0:5, 0:5] = Ha
132
+ H[5:10, 5:10] = Hc
133
+ H[0:5, 5:10] = Hac
134
+ H[5:10, 0:5] = Hac.conj().T
135
+ return H
136
+
137
+
138
+ def direct_gap_at_gamma(material):
139
+ """Conduction-band-minimum minus valence-band-maximum at k=Gamma,
140
+ for the 8 valence electrons (4 lowest of the 8 sp3-derived bands
141
+ filled; the 2 s* bands are always empty conduction states in this
142
+ model). Only meaningful as *the* fundamental gap for direct-gap
143
+ materials (e.g. GaAs) -- for indirect-gap materials (e.g. Si) this
144
+ is the Gamma-Gamma transition, not the true (lower) indirect gap,
145
+ which occurs off Gamma and this function does not compute."""
146
+ H = sp3s_star_hamiltonian((0.0, 0.0, 0.0), material)
147
+ eig = np.sort(np.linalg.eigvalsh(H).real)
148
+ return eig[4] - eig[3]
149
+
150
+
151
+ def band_extrema_along_path(material, k_start, k_end, n_points=501):
152
+ """
153
+ Scans the valence-band maximum and conduction-band minimum (bands 3
154
+ and 4 of the 10, 0-indexed, sorted) along the straight line from
155
+ k_start to k_end (both in the same 2*pi/a cubic-axis units as
156
+ sp3s_star_hamiltonian), and returns
157
+ (vbm, vbm_k, cbm, cbm_k, gap) where vbm_k/cbm_k are the k-points
158
+ (same units) where each extremum was found.
159
+
160
+ Needed for indirect-gap materials (e.g. Si, Ge): the true
161
+ fundamental gap is not at Gamma, so direct_gap_at_gamma alone gives
162
+ the wrong (too-large) number for them. Example: Si's conduction
163
+ band minimum sits off-Gamma along Gamma->X (the Delta line), not
164
+ at Gamma itself.
165
+ """
166
+ if isinstance(material, str):
167
+ if material not in MATERIALS:
168
+ raise ValueError(f"no VHD parameters for {material!r}; available: {sorted(MATERIALS)}")
169
+ material = MATERIALS[material]
170
+
171
+ k_start = np.asarray(k_start, dtype=float)
172
+ k_end = np.asarray(k_end, dtype=float)
173
+ t = np.linspace(0.0, 1.0, n_points)
174
+ ks = k_start[None, :] + t[:, None] * (k_end - k_start)[None, :]
175
+
176
+ vbm_per_k = np.empty(n_points)
177
+ cbm_per_k = np.empty(n_points)
178
+ for i, k in enumerate(ks):
179
+ eig = np.sort(np.linalg.eigvalsh(sp3s_star_hamiltonian(k, material)).real)
180
+ vbm_per_k[i] = eig[3]
181
+ cbm_per_k[i] = eig[4]
182
+
183
+ vbm_idx = vbm_per_k.argmax()
184
+ cbm_idx = cbm_per_k.argmin()
185
+ vbm, vbm_k = vbm_per_k[vbm_idx], ks[vbm_idx]
186
+ cbm, cbm_k = cbm_per_k[cbm_idx], ks[cbm_idx]
187
+ return vbm, vbm_k, cbm, cbm_k, cbm - vbm
@@ -0,0 +1,9 @@
1
+ """Backward-compatibility shim -- the real implementation moved to
2
+ dense_evolution.physics.states as part of the Phase 2 subpackage split
3
+ (see prog.txt). Kept so `from dense_evolution.states import ghz_state`
4
+ (used by external consumers, e.g. Dense-Evolution-Discovery) keeps working
5
+ unchanged. Import from dense_evolution.physics.states directly in new code.
6
+ """
7
+ from dense_evolution.physics.states import ghz_state
8
+
9
+ __all__ = ['ghz_state']
@@ -0,0 +1,9 @@
1
+ """Backward-compatibility shim -- the real implementation moved to
2
+ dense_evolution.circuits.topology as part of the Phase 2 subpackage split
3
+ (see prog.txt). Kept so `from dense_evolution.topology import entangling_layer`
4
+ (used by external consumers, e.g. Dense-Evolution-Discovery) keeps working
5
+ unchanged. Import from dense_evolution.circuits.topology directly in new code.
6
+ """
7
+ from dense_evolution.circuits.topology import entangling_layer, VALID_PATTERNS
8
+
9
+ __all__ = ['entangling_layer', 'VALID_PATTERNS']
@@ -0,0 +1,9 @@
1
+ """Backward-compatibility shim -- the real implementation moved to
2
+ dense_evolution.circuits.trotter as part of the Phase 2 subpackage split
3
+ (see prog.txt). Kept so `from dense_evolution.trotter import pauli_rotation_ops`
4
+ (used by external consumers, e.g. Dense-Evolution-Discovery) keeps working
5
+ unchanged. Import from dense_evolution.circuits.trotter directly in new code.
6
+ """
7
+ from dense_evolution.circuits.trotter import pauli_rotation_ops, trotter_evolve_ops
8
+
9
+ __all__ = ['pauli_rotation_ops', 'trotter_evolve_ops']
@@ -0,0 +1,13 @@
1
+ """Utility subpackage: circuit drawing and measurement/sampling helpers.
2
+
3
+ Part of the Phase 2 subpackage split (see prog.txt) -- `utils/` is the
4
+ first subpackage moved, chosen because it has no internal dense_evolution
5
+ dependents (leaf module), matching dynamiqs's own `core/` vs `apis/`
6
+ split philosophy applied here as "role" grouping rather than "physics
7
+ domain" grouping.
8
+ """
9
+
10
+ from .drawing import draw_circuit
11
+ from .measurement import sample_counts, statevector_fidelity
12
+
13
+ __all__ = ["draw_circuit", "sample_counts", "statevector_fidelity"]
@@ -0,0 +1,101 @@
1
+ """
2
+ Plain-text circuit diagrams -- the fast way to sanity-check what a
3
+ gate-tuple list actually builds without running it, the same job
4
+ Qiskit's `circuit.draw()` or Cirq's text diagrams do.
5
+
6
+ Uses plain ASCII ('-', '|', '*') rather than Unicode box-drawing
7
+ characters deliberately: a diagram meant to be printed to a terminal or
8
+ written to a log file needs to survive whatever console encoding the
9
+ caller happens to have (this was tested against, and would otherwise
10
+ crash on, a default Windows cp1252 console).
11
+ """
12
+
13
+ __all__ = ['draw_circuit']
14
+
15
+ _LABELS = {
16
+ 'h': 'H', 'x': 'X', 'y': 'Y', 'z': 'Z', 's': 'S', 'sdg': 'Sdg',
17
+ 't': 'T', 'tdg': 'Tdg', 'sx': 'Sx', 'id': 'I',
18
+ 'rx': 'Rx', 'ry': 'Ry', 'rz': 'Rz', 'p': 'P', 'u1': 'P', 'phase': 'P',
19
+ }
20
+ _TWO_QUBIT_TARGET_SYMBOL = {'cx': 'X', 'cz': 'Z', 'cy': 'Y', 'cp': 'P', 'crz': 'Rz'}
21
+
22
+
23
+ def draw_circuit(circuit, n_qubits):
24
+ """
25
+ Render a circuit (in the gate-tuple form run_circuit accepts) as a
26
+ plain-text diagram, one gate per column, one line per qubit.
27
+
28
+ Parameters
29
+ ----------
30
+ circuit : list[tuple]
31
+ Gate ops, as passed to `DenseSVSimulator.run_circuit`. Supports
32
+ every gate this package's transpiler recognizes: single-qubit
33
+ static and parametric gates, cx/cz/cy/cp/crz, swap, and ccx.
34
+ n_qubits : int
35
+ Number of qubit rows to draw, must be >= 1 (should cover every
36
+ qubit index referenced in `circuit`).
37
+
38
+ Returns
39
+ -------
40
+ str
41
+ Multi-line diagram, one row per qubit (``q0: --H----*--``, ...).
42
+ Print it, or split on '\\n' to inspect individual rows.
43
+
44
+ Examples
45
+ --------
46
+ >>> import dense_evolution as de
47
+ >>> print(de.draw_circuit([('h', 0), ('cx', 0, 1)], n_qubits=2))
48
+ q0: --H----*--
49
+ q1: -------X--
50
+ """
51
+ if n_qubits < 1:
52
+ raise ValueError(f"draw_circuit needs at least 1 qubit, got {n_qubits}")
53
+
54
+ prefixes = [f"q{q}: " for q in range(n_qubits)]
55
+ prefix_width = max(len(p) for p in prefixes)
56
+ rows = [p.ljust(prefix_width) for p in prefixes]
57
+
58
+ for op in circuit:
59
+ name = op[0]
60
+ col = {} # qubit -> symbol to draw in this column (None = plain wire)
61
+
62
+ if name == 'swap':
63
+ a, b = op[1], op[2]
64
+ lo, hi = min(a, b), max(a, b)
65
+ for q in range(n_qubits):
66
+ col[q] = 'x' if q in (a, b) else ('|' if lo < q < hi else None)
67
+ elif name == 'ccx':
68
+ c1, c2, tgt = op[1], op[2], op[3]
69
+ lo, hi = min(c1, c2, tgt), max(c1, c2, tgt)
70
+ for q in range(n_qubits):
71
+ if q in (c1, c2):
72
+ col[q] = '*'
73
+ elif q == tgt:
74
+ col[q] = 'X'
75
+ else:
76
+ col[q] = '|' if lo < q < hi else None
77
+ elif name in _TWO_QUBIT_TARGET_SYMBOL:
78
+ ctrl, tgt = op[1], op[2]
79
+ lo, hi = min(ctrl, tgt), max(ctrl, tgt)
80
+ for q in range(n_qubits):
81
+ if q == ctrl:
82
+ col[q] = '*'
83
+ elif q == tgt:
84
+ col[q] = _TWO_QUBIT_TARGET_SYMBOL[name]
85
+ else:
86
+ col[q] = '|' if lo < q < hi else None
87
+ else:
88
+ q = op[1]
89
+ col[q] = _LABELS.get(name, name.upper())
90
+
91
+ label_width = max((len(v) for v in col.values() if v is not None), default=1)
92
+ seg_width = label_width + 4 # 2 filler chars on each side
93
+
94
+ for q in range(n_qubits):
95
+ content = col.get(q)
96
+ if content is None:
97
+ rows[q] += '-' * seg_width
98
+ else:
99
+ rows[q] += '--' + content.center(label_width) + '--'
100
+
101
+ return '\n'.join(rows)
@@ -0,0 +1,246 @@
1
+ """
2
+ Exact bounded mass-decomposition scoring: given a target mass difference
3
+ and a molecular formula's atom budget, find the closest achievable
4
+ sub-formula mass and its RDBE (degree-of-unsaturation) validity.
5
+
6
+ Promoted from Dense-Evolution-Discovery's CASMI26 spectral-identification
7
+ experiments (real MS/MS mass-spectrometry data, OTRF/Enveda CASMI26
8
+ Kaggle dataset). Real, verified need: testing whether a peak-to-peak mass
9
+ difference in a spectrum corresponds to a chemically real neutral loss
10
+ requires knowing whether SOME integer combination of the precursor's own
11
+ atoms reaches that mass -- a bounded integer subset-sum problem, solved
12
+ here exactly via iterative Minkowski sums (not a continuous relaxation:
13
+ the element-count space is small enough, typically under 10^5 states,
14
+ to enumerate directly).
15
+
16
+ A candidate decomposition is chemically valid only if its Ring-plus-
17
+ Double-Bond-Equivalent (degree of unsaturation) is non-negative:
18
+
19
+ RDBE = 1 + sum_i( count_i * (valence_i - 2) ) / 2
20
+
21
+ (standard organic chemistry -- the same closed-shell constraint already
22
+ used in this package's own isodesmic bond-scission work). Verified on
23
+ real CASMI26 data: without this filter, an unrelated (wrong) molecule's
24
+ formula "explains" 58.5% of real peak-pair differences by coincidence
25
+ alone (mean, n=1952 real spectra) -- RDBE-filtering doesn't remove that
26
+ gap, but does remove chemically impossible matches from both sides,
27
+ so any downstream feature/threshold built on this function is scoring
28
+ real, buildable neutral losses only, not arithmetic coincidences.
29
+
30
+ build_reachable_density_fft below builds the same reachable-mass
31
+ landscape a second way, via the convolution theorem (an FFT instead of
32
+ direct enumeration) with a physically motivated ppm-level Gaussian
33
+ tolerance instead of a fixed absolute one -- same physics, a different
34
+ (and for large state spaces, cheaper) construction.
35
+ """
36
+ import re
37
+ from typing import Dict, Optional, Tuple
38
+
39
+ import numpy as np
40
+
41
+ __all__ = [
42
+ "parse_formula", "rdbe", "build_reachable_masses", "nearest_reachable_mass",
43
+ "build_reachable_density_fft", "density_at_mass",
44
+ ]
45
+
46
+ ATOMIC_MASS: Dict[str, float] = {
47
+ "C": 12.000000, "H": 1.007825, "N": 14.003074, "O": 15.994915,
48
+ "S": 31.972071, "P": 30.973762, "Cl": 34.968853, "F": 18.998403,
49
+ "Br": 78.918338, "I": 126.904473, "Na": 22.989770, "K": 38.963707,
50
+ "Si": 27.976927, "B": 11.009305,
51
+ }
52
+ VALENCE: Dict[str, int] = {
53
+ "C": 4, "H": 1, "N": 3, "O": 2, "S": 2, "P": 3, "Cl": 1, "F": 1,
54
+ "Br": 1, "I": 1, "Na": 1, "K": 1, "Si": 4, "B": 3,
55
+ }
56
+
57
+ _FORMULA_TOKEN_RE = re.compile(r"([A-Z][a-z]?)(\d*)")
58
+
59
+
60
+ def parse_formula(formula: str) -> Dict[str, int]:
61
+ """Parse a molecular formula string (e.g. "C20H15N3O2") into
62
+ {element: count}. Repeated elements in the string are summed,
63
+ matching how real formula strings from different sources
64
+ occasionally repeat a symbol instead of merging it. Elements not in
65
+ ATOMIC_MASS/VALENCE are kept in the returned dict (parsing doesn't
66
+ know or care which elements downstream functions recognize) --
67
+ rdbe()/build_reachable_masses()/build_reachable_density_fft() each
68
+ skip an unrecognized element explicitly, not this function."""
69
+ counts: Dict[str, int] = {}
70
+ for elem, num in _FORMULA_TOKEN_RE.findall(formula or ""):
71
+ counts[elem] = counts.get(elem, 0) + (int(num) if num else 1)
72
+ return counts
73
+
74
+
75
+ def rdbe(formula_counts: Dict[str, int]) -> float:
76
+ """Ring-plus-Double-Bond-Equivalent (degree of unsaturation) of a
77
+ formula: RDBE = 1 + sum(count_i * (valence_i - 2)) / 2. A real,
78
+ non-negative closed-shell molecule/fragment always has RDBE >= 0;
79
+ negative RDBE means the atom counts cannot form any valid closed-
80
+ shell structure (too many univalent atoms for the given carbon/
81
+ nitrogen skeleton) -- unrecognized elements are ignored, not
82
+ counted as an error, since a formula may legitimately include an
83
+ element this module has no valence table entry for."""
84
+ acc = 0
85
+ for elem, count in formula_counts.items():
86
+ val = VALENCE.get(elem)
87
+ if val is not None and count > 0:
88
+ acc += count * (val - 2)
89
+ return 1.0 + acc / 2.0
90
+
91
+
92
+ def build_reachable_masses(
93
+ formula_counts: Dict[str, int],
94
+ max_mass: float,
95
+ resolution: float = 0.0005,
96
+ require_rdbe_valid: bool = True,
97
+ max_states: int = 150_000,
98
+ ) -> np.ndarray:
99
+ """All sub-formula masses reachable by using 0..count_i atoms of
100
+ each element in formula_counts (0 <= f <= formula_counts,
101
+ component-wise), sorted ascending. Exact bounded subset-sum via
102
+ iterative Minkowski sums over each element in turn -- correct
103
+ because element counts here are small enough (real molecules,
104
+ typically under a few hundred atoms total) that the reachable set
105
+ never needs more than max_states entries after resolution-binning.
106
+
107
+ require_rdbe_valid=True (default) keeps, for each mass bin, only
108
+ whether ANY chemically valid (RDBE >= 0) combination reaches it --
109
+ the physically meaningful question for neutral-loss matching.
110
+ Pass False to get the pure arithmetic reachable set instead (e.g.
111
+ for a sanity check against a hand-verified subset-sum test case).
112
+
113
+ max_mass bounds the search (usually the precursor's own observed
114
+ mass) so element counts far beyond what's physically possible for
115
+ that precursor are never explored."""
116
+ import pandas as pd # local import: only this function needs it
117
+
118
+ reach = np.array([0.0])
119
+ rdbe2 = np.array([0]) # 2*(RDBE-1); RDBE = 1 + rdbe2/2, starts at rdbe2=0 (empty formula, RDBE=1)
120
+
121
+ for elem, count in formula_counts.items():
122
+ mass = ATOMIC_MASS.get(elem)
123
+ val = VALENCE.get(elem)
124
+ if mass is None or count <= 0:
125
+ continue
126
+ count = min(count, 60) # sanity cap; real formulas here are far below this
127
+ incr_mass = np.arange(0, count + 1) * mass
128
+ incr_rdbe2 = np.arange(0, count + 1) * ((val - 2) if val is not None else 0)
129
+
130
+ reach = np.add.outer(reach, incr_mass).ravel()
131
+ rdbe2 = np.add.outer(rdbe2, incr_rdbe2).ravel()
132
+
133
+ # No emptiness guard needed here: the "use zero atoms of this
134
+ # element" branch (incr_mass's k=0 term) always carries every
135
+ # prior surviving entry through unchanged, and RDBE contribution
136
+ # 0 always satisfies `>= -2` -- so `reach`/`rdbe2` can shrink but
137
+ # never become empty as long as they started non-empty (which
138
+ # the initial reach=[0.0] guarantees for any max_mass >= -1).
139
+ keep = reach <= max_mass + 1.0
140
+ reach, rdbe2 = reach[keep], rdbe2[keep]
141
+
142
+ bins = np.round(reach / resolution).astype(np.int64)
143
+ if require_rdbe_valid:
144
+ valid = rdbe2 >= -2 # RDBE >= 0 <=> 1 + rdbe2/2 >= 0
145
+ bins, reach, rdbe2 = bins[valid], reach[valid], rdbe2[valid]
146
+ df = pd.DataFrame({"bin": bins, "mass": reach, "rdbe2": rdbe2})
147
+ df = df.drop_duplicates(subset="bin")
148
+ if len(df) > max_states:
149
+ df = df.iloc[:: max(1, len(df) // max_states)]
150
+ reach, rdbe2 = df["mass"].to_numpy(), df["rdbe2"].to_numpy()
151
+
152
+ return np.sort(reach)
153
+
154
+
155
+ def nearest_reachable_mass(target: float, reachable_sorted: np.ndarray) -> Optional[float]:
156
+ """The single closest value in a (sorted, ascending) reachable-mass
157
+ array to `target`, or None if the array is empty/trivial (only the
158
+ zero-formula placeholder). O(log n) via binary search."""
159
+ if reachable_sorted.size <= 1:
160
+ return None
161
+ i = np.searchsorted(reachable_sorted, target)
162
+ candidates = [k for k in (i - 1, i) if 0 <= k < reachable_sorted.size]
163
+ if not candidates:
164
+ return None
165
+ return float(min((reachable_sorted[k] for k in candidates), key=lambda m: abs(m - target)))
166
+
167
+
168
+ def build_reachable_density_fft(
169
+ formula_counts: Dict[str, int],
170
+ max_mass: float,
171
+ grid_resolution: float = 0.001,
172
+ relative_tolerance: float = 2e-5,
173
+ ) -> Tuple[np.ndarray, np.ndarray]:
174
+ """Plausibility density over achievable sub-formula masses, via the
175
+ convolution theorem instead of the exact Minkowski-sum enumeration
176
+ in build_reachable_masses.
177
+
178
+ Real precedent, not a novel trick: this is the same FFT technique
179
+ Rockwood & Van Orden use to compute isotope distributions
180
+ (Anal. Chem. 1996, "Ultrahigh-Speed Calculation of Isotope
181
+ Distributions") -- using 0..count_i copies of element i as a spike
182
+ train instead of an isotope-abundance distribution, but the same
183
+ convolution-theorem trick applies identically: the reachable-mass
184
+ landscape of "any combination of atoms up to the formula's budget"
185
+ is the CONVOLUTION of each element's own spike train, and
186
+ convolution in mass-space is a plain pointwise PRODUCT in frequency
187
+ space -- so combining K element types costs K FFTs and one inverse
188
+ FFT, not an enumeration over the (potentially huge) product of
189
+ per-element choices that build_reachable_masses must truncate
190
+ (max_states) for.
191
+
192
+ A single Gaussian broadening is applied ONCE, after combining every
193
+ element -- not per-element -- because real mass-measurement
194
+ uncertainty (ppm-level, `relative_tolerance`) is a property of the
195
+ FINAL observed mass difference, not of each element's contribution
196
+ individually. This replaces build_reachable_masses's arbitrary fixed
197
+ absolute tolerance with a physically motivated relative (ppm) one.
198
+
199
+ Returns (mass_grid, density). density is a plausibility landscape,
200
+ not a normalized probability distribution: many sub-formulas can
201
+ coincide near the same mass and add constructively, so its peak
202
+ height reflects how many/how well-supported combinations reach that
203
+ mass, not a probability that sums to 1.
204
+
205
+ See test_mass_decomposition.py for a direct verification that this
206
+ FFT-based construction agrees with brute-force convolution and with
207
+ build_reachable_masses's own exact enumeration on the same formula.
208
+ """
209
+ n_points = int(max_mass / grid_resolution) + 2
210
+ combined_fft = None
211
+
212
+ for elem, count in formula_counts.items():
213
+ mass = ATOMIC_MASS.get(elem)
214
+ if mass is None or count <= 0:
215
+ continue
216
+ count = min(count, 60)
217
+ spikes = np.zeros(n_points)
218
+ for k in range(count + 1):
219
+ idx = int(round(k * mass / grid_resolution))
220
+ if idx < n_points:
221
+ spikes[idx] += 1.0
222
+ elem_fft = np.fft.rfft(spikes)
223
+ combined_fft = elem_fft if combined_fft is None else combined_fft * elem_fft
224
+
225
+ mass_grid = np.arange(n_points) * grid_resolution
226
+ if combined_fft is None:
227
+ return mass_grid, np.zeros(n_points)
228
+
229
+ density = np.fft.irfft(combined_fft, n=n_points)
230
+ density = np.maximum(density, 0.0) # guards float round-off, true density is never negative
231
+
232
+ sigma_mass = max(relative_tolerance * max_mass, grid_resolution)
233
+ sigma_points = sigma_mass / grid_resolution
234
+ half = max(int(6 * sigma_points), 1)
235
+ kx = np.arange(-half, half + 1)
236
+ kernel = np.exp(-0.5 * (kx / sigma_points) ** 2)
237
+ kernel /= kernel.sum()
238
+ density = np.convolve(density, kernel, mode="same")
239
+
240
+ return mass_grid, density
241
+
242
+
243
+ def density_at_mass(mass_grid: np.ndarray, density: np.ndarray, target: float) -> float:
244
+ """Linearly-interpolated plausibility density at an arbitrary target
245
+ mass (need not land exactly on the grid)."""
246
+ return float(np.interp(target, mass_grid, density, left=0.0, right=0.0))
@@ -0,0 +1,94 @@
1
+ """
2
+ Measurement-related conveniences: turning a statevector into finite-shot
3
+ counts the way real hardware and every other SDK report results (Qiskit's
4
+ `get_counts()`, PennyLane's `qml.counts()`), and comparing two pure states
5
+ directly without building a density matrix first.
6
+
7
+ Sampling a statevector into shot counts is another pattern found
8
+ hand-duplicated across this package's own experiment scripts -- always
9
+ the same `rng.choice(dim, p=probs)` + `np.bincount` (or `np.unique`)
10
+ couple of lines, rewritten fresh each time.
11
+ """
12
+ import numpy as np
13
+
14
+ __all__ = ['sample_counts', 'statevector_fidelity']
15
+
16
+
17
+ def sample_counts(statevector, n_shots, rng=None):
18
+ """
19
+ Simulate n_shots projective measurements of every qubit in the
20
+ computational basis, returning a Qiskit-style counts dict.
21
+
22
+ Parameters
23
+ ----------
24
+ statevector : array-like, shape (2**n_qubits,)
25
+ n_shots : int
26
+ Number of measurement shots to sample, must be >= 1.
27
+ rng : numpy.random.Generator, optional
28
+ A fresh `numpy.random.default_rng()` is used if not given --
29
+ pass one explicitly for reproducible sampling.
30
+
31
+ Returns
32
+ -------
33
+ dict[str, int]
34
+ Bitstring keys read left-to-right as qubit 0 upward (matching
35
+ DenseSVSimulator's own qubit-0-is-MSB layout and
36
+ pauli_expectation's string convention), each mapped to how many
37
+ of the n_shots samples landed on it. Only bitstrings with at
38
+ least one hit appear (unobserved outcomes are simply absent, as
39
+ in Qiskit's get_counts()).
40
+
41
+ Examples
42
+ --------
43
+ >>> import dense_evolution as de
44
+ >>> sim = de.DenseSVSimulator(2)
45
+ >>> sim.run_circuit([('h', 0), ('cx', 0, 1)])
46
+ >>> de.sample_counts(sim.get_statevector(), 1000)
47
+ {'00': 494, '11': 506}
48
+ """
49
+ statevector = np.asarray(statevector)
50
+ dim = statevector.shape[0]
51
+ n_qubits = dim.bit_length() - 1
52
+ if 1 << n_qubits != dim:
53
+ raise ValueError(f"statevector length {dim} is not a power of 2")
54
+ if n_shots < 1:
55
+ raise ValueError(f"n_shots must be >= 1, got {n_shots}")
56
+
57
+ probs = np.abs(statevector) ** 2
58
+ total = probs.sum()
59
+ if not np.isfinite(total) or total <= 0:
60
+ raise ValueError("statevector has zero or non-finite total probability")
61
+ probs = probs / total
62
+
63
+ if rng is None:
64
+ rng = np.random.default_rng()
65
+ outcomes = rng.choice(dim, size=n_shots, p=probs)
66
+ values, freqs = np.unique(outcomes, return_counts=True)
67
+
68
+ return {format(int(v), f'0{n_qubits}b'): int(f) for v, f in zip(values, freqs)}
69
+
70
+
71
+ def statevector_fidelity(statevector_a, statevector_b):
72
+ """
73
+ Fidelity |<a|b>|^2 between two pure statevectors -- the cheap, direct
74
+ pure-state counterpart to `uhlmann_fidelity` (which compares two full
75
+ density matrices, needed for the mixed/noisy states that
76
+ `zne_density_matrix` and friends work with). Use this one whenever
77
+ both states are pure; it never builds an O(dim^2) density matrix.
78
+
79
+ Parameters
80
+ ----------
81
+ statevector_a, statevector_b : array-like, shape (2**n_qubits,)
82
+ Must have the same shape.
83
+
84
+ Returns
85
+ -------
86
+ float
87
+ In [0, 1] up to floating-point error. 1.0 for identical states
88
+ (up to global phase), 0.0 for orthogonal states.
89
+ """
90
+ a = np.asarray(statevector_a)
91
+ b = np.asarray(statevector_b)
92
+ if a.shape != b.shape:
93
+ raise ValueError(f"statevector shapes differ: {a.shape} vs {b.shape}")
94
+ return float(np.abs(np.vdot(a, b)) ** 2)