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.
- dashboard_core/__init__.py +115 -0
- dashboard_core/_gate_tables.py +30 -0
- dashboard_core/band_structure.py +71 -0
- dashboard_core/circuit_builder_component.py +232 -0
- dashboard_core/circuit_diagram.py +216 -0
- dashboard_core/crypto_protocols.py +77 -0
- dashboard_core/engine.py +326 -0
- dashboard_core/graphical_builder.py +114 -0
- dashboard_core/hamiltonians.py +593 -0
- dashboard_core/mass_decomposition_tool.py +47 -0
- dashboard_core/mitigation.py +343 -0
- dashboard_core/native_hf_diagnostics.py +62 -0
- dashboard_core/noise_tools.py +125 -0
- dashboard_core/qasm_library.py +233 -0
- dashboard_core/qmmm.py +16 -0
- dashboard_core/rag_tool.py +45 -0
- dashboard_core/state_visuals.py +288 -0
- dashboard_core/system_limits.py +60 -0
- dashboard_core/vector_healing.py +102 -0
- dashboard_core/visuals.py +158 -0
- dashboard_core/vqe.py +533 -0
- dashboard_core/wormhole.py +580 -0
- dense_evolution/__init__.py +114 -0
- dense_evolution/autodiff.py +10 -0
- dense_evolution/backends/__init__.py +5 -0
- dense_evolution/backends/chunk/__init__.py +37 -0
- dense_evolution/backends/chunk/_engine_imports.py +57 -0
- dense_evolution/backends/chunk/circuit_chunker.py +55 -0
- dense_evolution/backends/chunk/core.py +432 -0
- dense_evolution/backends/chunk/disk_overflow.py +232 -0
- dense_evolution/backends/chunk/geometry.py +95 -0
- dense_evolution/backends/chunk/guard.py +190 -0
- dense_evolution/backends/chunk/kernels.py +531 -0
- dense_evolution/backends/mps.py +1569 -0
- dense_evolution/backends/statevector.py +616 -0
- dense_evolution/chunk.py +25 -0
- dense_evolution/circuits/__init__.py +20 -0
- dense_evolution/circuits/compiler.py +488 -0
- dense_evolution/circuits/diagram.py +94 -0
- dense_evolution/circuits/gates.py +91 -0
- dense_evolution/circuits/parser.py +632 -0
- dense_evolution/circuits/qft.py +66 -0
- dense_evolution/circuits/random_circuit.py +85 -0
- dense_evolution/circuits/registry.py +74 -0
- dense_evolution/circuits/topology.py +79 -0
- dense_evolution/circuits/trotter.py +265 -0
- dense_evolution/circuits/uccsd.py +275 -0
- dense_evolution/cli.py +199 -0
- dense_evolution/compiler.py +9 -0
- dense_evolution/config.py +49 -0
- dense_evolution/drawing.py +10 -0
- dense_evolution/entropy.py +9 -0
- dense_evolution/fermions.py +9 -0
- dense_evolution/gates.py +9 -0
- dense_evolution/harrison_tb.py +16 -0
- dense_evolution/healing.py +18 -0
- dense_evolution/interop/__init__.py +18 -0
- dense_evolution/interop/qiskit_pennylane.py +406 -0
- dense_evolution/measurement.py +10 -0
- dense_evolution/mitigation/__init__.py +54 -0
- dense_evolution/mitigation/healing.py +215 -0
- dense_evolution/mitigation/kl_divergence.py +93 -0
- dense_evolution/mitigation/magic_entropy.py +163 -0
- dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
- dense_evolution/mitigation/renyi.py +168 -0
- dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
- dense_evolution/mitigation/zne.py +990 -0
- dense_evolution/mps.py +9 -0
- dense_evolution/native_hf/__init__.py +26 -0
- dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
- dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
- dense_evolution/native_hf/assembly.py +304 -0
- dense_evolution/native_hf/basis.py +117 -0
- dense_evolution/native_hf/boys.py +35 -0
- dense_evolution/native_hf/bridge.py +112 -0
- dense_evolution/native_hf/cartesian.py +64 -0
- dense_evolution/native_hf/coulomb.py +196 -0
- dense_evolution/native_hf/differentiable.py +53 -0
- dense_evolution/native_hf/gaussians.py +79 -0
- dense_evolution/native_hf/kinetic.py +52 -0
- dense_evolution/native_hf/libcint_bridge.py +167 -0
- dense_evolution/native_hf/overlap.py +91 -0
- dense_evolution/native_hf/scf.py +404 -0
- dense_evolution/noise/__init__.py +79 -0
- dense_evolution/noise/coherent_attack.py +264 -0
- dense_evolution/noise/cosmic_ray.py +61 -0
- dense_evolution/noise/density_matrix_channels.py +78 -0
- dense_evolution/noise/differentiable.py +66 -0
- dense_evolution/noise/kraus/__init__.py +6 -0
- dense_evolution/noise/kraus/amplitude_damping.py +47 -0
- dense_evolution/noise/kraus/bitflip.py +22 -0
- dense_evolution/noise/kraus/combined.py +16 -0
- dense_evolution/noise/kraus/depolarizing.py +47 -0
- dense_evolution/noise/kraus/ideal.py +10 -0
- dense_evolution/noise/kraus/phaseflip.py +21 -0
- dense_evolution/noise/kraus_channels.py +285 -0
- dense_evolution/noise/oscillating.py +32 -0
- dense_evolution/noise/pink.py +80 -0
- dense_evolution/observables.py +11 -0
- dense_evolution/parser.py +9 -0
- dense_evolution/physics/__init__.py +27 -0
- dense_evolution/physics/entropy.py +161 -0
- dense_evolution/physics/fermions.py +322 -0
- dense_evolution/physics/observables.py +523 -0
- dense_evolution/physics/qec.py +1113 -0
- dense_evolution/physics/spectral.py +143 -0
- dense_evolution/physics/states.py +43 -0
- dense_evolution/protocols/__init__.py +27 -0
- dense_evolution/protocols/bb84.py +133 -0
- dense_evolution/protocols/di_qkd_ghz.py +199 -0
- dense_evolution/protocols/dicka_protocol2.py +124 -0
- dense_evolution/qec.py +20 -0
- dense_evolution/qft.py +9 -0
- dense_evolution/qmmm/__init__.py +13 -0
- dense_evolution/qmmm/ase_bridge.py +97 -0
- dense_evolution/qmmm/forces.py +388 -0
- dense_evolution/qmmm/propagation.py +80 -0
- dense_evolution/qmmm/region.py +137 -0
- dense_evolution/random_circuit.py +15 -0
- dense_evolution/registry.py +9 -0
- dense_evolution/simulator.py +10 -0
- dense_evolution/solvers/__init__.py +19 -0
- dense_evolution/solvers/autodiff.py +169 -0
- dense_evolution/solvers/harrison_tb.py +189 -0
- dense_evolution/solvers/vhd_tb.py +187 -0
- dense_evolution/states.py +9 -0
- dense_evolution/topology.py +9 -0
- dense_evolution/trotter.py +9 -0
- dense_evolution/utils/__init__.py +13 -0
- dense_evolution/utils/drawing.py +101 -0
- dense_evolution/utils/mass_decomposition.py +246 -0
- dense_evolution/utils/measurement.py +94 -0
- dense_evolution/vhd_tb.py +16 -0
- dense_evolution-8.3.0.dist-info/METADATA +366 -0
- dense_evolution-8.3.0.dist-info/RECORD +165 -0
- dense_evolution-8.3.0.dist-info/WHEEL +5 -0
- dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
- dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
- dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
- ia_utils/__init__.py +0 -0
- ia_utils/adversarial_vector_attack.py +196 -0
- ia_utils/rag.py +288 -0
- ia_utils/vector_healing.py +399 -0
- local_site/__init__.py +0 -0
- local_site/app/__init__.py +0 -0
- local_site/app/server.py +1009 -0
- mcp_server/__init__.py +0 -0
- mcp_server/client.py +324 -0
- mcp_server/config.py +32 -0
- mcp_server/models.py +347 -0
- mcp_server/molecules.py +71 -0
- mcp_server/server.py +119 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/chemistry_tools.py +225 -0
- mcp_server/tools/circuit_tools.py +83 -0
- mcp_server/tools/crypto_tools.py +66 -0
- mcp_server/tools/mitigation_tools.py +81 -0
- mcp_server/tools/noise_tools.py +60 -0
- mcp_server/tools/retrieval_tools.py +44 -0
- mcp_server/tools/system_tools.py +149 -0
- mcp_server/tools/wormhole_tools.py +142 -0
- mcp_server/utils/__init__.py +0 -0
- mcp_server/utils/cache.py +55 -0
- mcp_server/utils/images.py +67 -0
- 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)
|