qdiffusivity 0.1.0__py3-none-any.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.
- qdiffusivity/__init__.py +51 -0
- qdiffusivity/binned.py +614 -0
- qdiffusivity/data/README.md +17 -0
- qdiffusivity/data/__init__.py +0 -0
- qdiffusivity/data/files.py +19 -0
- qdiffusivity/data/mda.txt +45 -0
- qdiffusivity/density.py +489 -0
- qdiffusivity/diffusivity.py +750 -0
- qdiffusivity/example_api.py +3 -0
- qdiffusivity/tests/__init__.py +0 -0
- qdiffusivity/tests/conftest.py +82 -0
- qdiffusivity/tests/test_binned.py +163 -0
- qdiffusivity/tests/test_density.py +123 -0
- qdiffusivity/tests/test_diffusivity.py +227 -0
- qdiffusivity/tests/test_qdiffusivity.py +8 -0
- qdiffusivity/tests/utils.py +77 -0
- qdiffusivity-0.1.0.dist-info/METADATA +123 -0
- qdiffusivity-0.1.0.dist-info/RECORD +22 -0
- qdiffusivity-0.1.0.dist-info/WHEEL +5 -0
- qdiffusivity-0.1.0.dist-info/licenses/AUTHORS.md +23 -0
- qdiffusivity-0.1.0.dist-info/licenses/LICENSE +21 -0
- qdiffusivity-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
▄▄████████▄▄
|
|
3
|
+
,▓███████████████▄ ____
|
|
4
|
+
▄███████████████████ ╔D╠╠╠╠╠╠╬φ_
|
|
5
|
+
▐█████████████████████ ╔╠╠╠╠╠╠╠╠╠╠╠╠▒
|
|
6
|
+
██████████████████████▌ ╠╠╠╠╠╠╠╠╠╠╠╠╠╠H
|
|
7
|
+
███████████████████████ ╠╠╠╠╠╠╠╠╠╠╠╠╠╠H
|
|
8
|
+
╙██████████████████████ ╠╠╠╠╠╠╠╠╠╠╠╠╠╩
|
|
9
|
+
╙██████████████████████ ╙╠╠╠╠╠╠╠╠╠╝^
|
|
10
|
+
'██████████████████████_ `'^╙╙^`
|
|
11
|
+
`▀███████████▀▀▀███████▄
|
|
12
|
+
`` _╓╗φ@@@φ╗╖,╙▀████▄▄╓,,,▄▄▄▓█████▓▄_
|
|
13
|
+
,φ▓╬╬╬╬╬╬╬╬╬╬╬╬▒,╙█████████████████████▓_
|
|
14
|
+
Æ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▌ ▀██████████████████████
|
|
15
|
+
_,,_ ╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓ ╚██████████████████████
|
|
16
|
+
╔╠╠╠╠╠╬_ [╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬L ██████████████████████▌
|
|
17
|
+
╠╠╠╠╠╠╠H ╟╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▒ ███████████████████████
|
|
18
|
+
`╚╠╠╠╠╙ ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬L ██████████████████████▌
|
|
19
|
+
[╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╩ ███████████████████████
|
|
20
|
+
é╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╜ ██████████████████████▀
|
|
21
|
+
_╗▓╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╝╙,▓████████████████████▀"
|
|
22
|
+
_╓╗╗╗╗╗╖,__ __╓φ▓╬╬╝╜"▄▄▄▄▄▄▄▄▄╠╙╙^""▄▄▓████▀" └╙╙▀▀▀""
|
|
23
|
+
╔Æ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╜╓▓█████████████████████▀
|
|
24
|
+
╗╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╩ ██████████████████████▀
|
|
25
|
+
╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▌ ██████████████████████Ñ ╔@DD╠DK╓
|
|
26
|
+
[╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬ ╟██████████████████████ ╒╠╠╠╠╠╠╠╠╠╠
|
|
27
|
+
╢╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬ ██████████████████████▌ ╚╠╠╠╠╠╠╠╠╠╠H
|
|
28
|
+
╚╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬ ╟█████████████████████` '╠╠╠╠╠╠╠╠╠╠
|
|
29
|
+
╫╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▒ ████████████████████▌ `╚╠╠╠╠╠╝╙
|
|
30
|
+
╚╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬W ██████████████████▀
|
|
31
|
+
╙╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓╖╙▀█████████████▀
|
|
32
|
+
"╙╨╝╝╝╨╙^`` `"╙╝╬╬▓W╓╙▀▀▀▀▀▀▀▀"_,,,__
|
|
33
|
+
_╓╦φφφφ╦,_ '╝╬╬╬╬▓▒▒▒▒▓╬╬╬╬╬╬╬╬╬▓╗_
|
|
34
|
+
,φ╠╠╠╠╠╠╠╠╠╠╠D╦_ `╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓,
|
|
35
|
+
╔╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╦ ╟╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▄
|
|
36
|
+
j╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠H ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬L
|
|
37
|
+
╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠ ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓
|
|
38
|
+
╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠ ║╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▌
|
|
39
|
+
²╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠ ╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬
|
|
40
|
+
'╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╩ `╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓
|
|
41
|
+
'╚╠╠╠╠╠╠╠╠╠╝╙ ╙╣╬╬╬╬╬╬╬╬╬╬╬╬╬╝^
|
|
42
|
+
````` `╙╨╝╝╝╝╝╨╙`
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
asciiart.club
|
qdiffusivity/density.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
r"""Kernel density estimator for transverse number-density profiles.
|
|
2
|
+
|
|
3
|
+
This module provides an Epanechnikov-kernel 1-D KDE with mirror-reflection
|
|
4
|
+
boundary handling and a Sheather-Jones plug-in bandwidth, packaged as an
|
|
5
|
+
:class:`~MDAnalysis.analysis.base.AnalysisBase` class so it can be applied
|
|
6
|
+
to any :class:`~MDAnalysis.core.groups.AtomGroup` along the confined axis of
|
|
7
|
+
a nanoconfined simulation.
|
|
8
|
+
|
|
9
|
+
The reusable KDE machinery (bandwidth selection, kernel evaluation, boundary
|
|
10
|
+
mirroring, Kish effective sample size) is generalised from the per-project
|
|
11
|
+
script ``zn-el/analysis/dens_kde/kde_density.py``; species selection, residue
|
|
12
|
+
COM pooling, mass-density conversion and plotting glue are left to the
|
|
13
|
+
caller.
|
|
14
|
+
|
|
15
|
+
Theory
|
|
16
|
+
------
|
|
17
|
+
For evaluation point :math:`z_0` and pooled samples :math:`\{z_j\}_{j=1}^{N}`,
|
|
18
|
+
|
|
19
|
+
.. math::
|
|
20
|
+
|
|
21
|
+
\hat\rho(z_0) = \frac{1}{N}\sum_j K_h(z_0 - z_j),
|
|
22
|
+
|
|
23
|
+
with the Epanechnikov kernel
|
|
24
|
+
|
|
25
|
+
.. math::
|
|
26
|
+
|
|
27
|
+
K_h(x) = \frac{3}{4h}\Bigl(1 - (x/h)^2\Bigr)\;\; \text{for } |x| < h,
|
|
28
|
+
\quad 0 \text{ otherwise}.
|
|
29
|
+
|
|
30
|
+
:math:`\hat\rho` integrates to 1 over the unbounded support.
|
|
31
|
+
|
|
32
|
+
* **Bandwidth.** ``"auto"`` uses a Sheather-Jones plug-in (an Epanechnikov
|
|
33
|
+
pilot density is built on a fine grid, its second derivative estimated by
|
|
34
|
+
central differences, and the oracle bandwidth
|
|
35
|
+
:math:`h^* = (\|K\|^2 / (N\,\mu_2(K)^2\,\widehat{\int[\hat f'']^2}))^{1/5}`
|
|
36
|
+
evaluated with the Epanechnikov constants :math:`\|K\|^2 = 3/5`,
|
|
37
|
+
:math:`\mu_2(K) = 1/5`); the Silverman rule of thumb is the fallback.
|
|
38
|
+
``"silverman"`` uses the rule of thumb directly, and a float fixes the
|
|
39
|
+
bandwidth.
|
|
40
|
+
|
|
41
|
+
* **Boundary handling.** Particles cannot cross the confined-region
|
|
42
|
+
boundaries, so kernel mass that would leak beyond :math:`[z_{\mathrm{bot}},
|
|
43
|
+
z_{\mathrm{top}}]` is mirrored back inside. For each sample at :math:`z_j`,
|
|
44
|
+
mirror copies at :math:`2z_{\mathrm{bot}} - z_j` and
|
|
45
|
+
:math:`2z_{\mathrm{top}} - z_j` are added before kernel evaluation.
|
|
46
|
+
|
|
47
|
+
* **Effective sample size.** The Kish effective sample size
|
|
48
|
+
:math:`N_{\mathrm{eff}}(z_0) = (\sum_j K_h)^2 / \sum_j K_h^2` is returned at
|
|
49
|
+
each grid point so unreliable regions (small local sample size) can be
|
|
50
|
+
masked.
|
|
51
|
+
|
|
52
|
+
* **Evaluation grid.** ``n_points`` cell-centred points uniformly across the
|
|
53
|
+
confined region, :math:`z_m = z_{\mathrm{bot}} + (m + 0.5)(z_{\mathrm{top}} -
|
|
54
|
+
z_{\mathrm{bot}})/M` for :math:`m = 0, \ldots, M-1`.
|
|
55
|
+
|
|
56
|
+
Converting the normalised KDE :math:`\hat\rho` (1/length, integrates to 1) to a
|
|
57
|
+
number density is the caller's responsibility; for a simulation with
|
|
58
|
+
cross-sectional area :math:`A` and :math:`N_f` analysis frames,
|
|
59
|
+
|
|
60
|
+
.. math::
|
|
61
|
+
|
|
62
|
+
n(z) = \frac{N_{\mathrm{total}}}{N_f\,A}\,\hat\rho(z),
|
|
63
|
+
|
|
64
|
+
and a mass density follows by multiplying by :math:`M_{\mathrm{mol}}/N_A`
|
|
65
|
+
(with the appropriate :math:`10^{24}` factor for Å → cm).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
from __future__ import annotations
|
|
69
|
+
|
|
70
|
+
import numpy as np
|
|
71
|
+
from MDAnalysis.analysis.base import AnalysisBase
|
|
72
|
+
|
|
73
|
+
# Epanechnikov kernel constants used by the Sheather-Jones plug-in.
|
|
74
|
+
# ||K||^2 = int K(u)^2 du = 3/5
|
|
75
|
+
# mu_2(K) = int u^2 K(u) du = 1/5
|
|
76
|
+
_EPA_KERNEL_NORM_SQ = 3.0 / 5.0
|
|
77
|
+
_EPA_KERNEL_MU2 = 1.0 / 5.0
|
|
78
|
+
_EPA_KERNEL_MU2_SQ = _EPA_KERNEL_MU2**2
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def silverman_bw(z_data):
|
|
82
|
+
r"""Silverman's rule of thumb.
|
|
83
|
+
|
|
84
|
+
Returns :math:`h = 1.06\,\hat\sigma\,N^{-1/5}` with the robust scaled-IQR
|
|
85
|
+
scale :math:`\hat\sigma = \min(\mathrm{std}, \mathrm{IQR}/1.34)` to guard
|
|
86
|
+
against heavy-tailed distributions. This is a Gaussian-reference rule of
|
|
87
|
+
thumb used as a fallback for the plug-in bandwidth; the KDE itself uses
|
|
88
|
+
the Epanechnikov kernel.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
z_data : array_like
|
|
93
|
+
1-D sample of positions.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
float
|
|
98
|
+
Bandwidth ``h`` (same units as ``z_data``).
|
|
99
|
+
"""
|
|
100
|
+
z = np.asarray(z_data, dtype=float).ravel()
|
|
101
|
+
n = z.size
|
|
102
|
+
if n < 2:
|
|
103
|
+
return 0.1
|
|
104
|
+
std = float(np.std(z, ddof=1))
|
|
105
|
+
q75, q25 = np.percentile(z, [75, 25])
|
|
106
|
+
iqr = q75 - q25
|
|
107
|
+
sigma_hat = min(std, iqr / 1.34) if iqr > 0 else std
|
|
108
|
+
if sigma_hat <= 0:
|
|
109
|
+
sigma_hat = std if std > 0 else 1.0
|
|
110
|
+
h = 1.06 * sigma_hat * n ** (-1.0 / 5.0)
|
|
111
|
+
return float(h)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sheather_jones_bw(z_data, z_lo, z_hi):
|
|
115
|
+
r"""Simplified Sheather-Jones plug-in bandwidth for the Epanechnikov kernel.
|
|
116
|
+
|
|
117
|
+
Stage 1 — *Pilot density.* The z-data is histogrammed with ~50 bins over
|
|
118
|
+
``[z_lo, z_hi]`` and smoothed with an Epanechnikov kernel of Silverman
|
|
119
|
+
bandwidth to obtain a pilot :math:`\hat f` on a fine grid.
|
|
120
|
+
|
|
121
|
+
Stage 2 — *Second derivative.* :math:`\hat f''` is estimated from the
|
|
122
|
+
pilot by central finite differences and
|
|
123
|
+
:math:`\widehat{\int[\hat f'']^2} = \sum \hat f''^2 \Delta z` computed.
|
|
124
|
+
|
|
125
|
+
Stage 3 — *Oracle bandwidth.*
|
|
126
|
+
|
|
127
|
+
.. math::
|
|
128
|
+
|
|
129
|
+
h^* = \Bigl(\frac{\|K\|^2}{N\,\mu_2(K)^2\,
|
|
130
|
+
\widehat{\int[\hat f'']^2}}\Bigr)^{1/5},
|
|
131
|
+
|
|
132
|
+
with the Epanechnikov constants. If the result is NaN, non-positive, or
|
|
133
|
+
larger than the Silverman fallback, :func:`silverman_bw` is returned.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
z_data : array_like
|
|
138
|
+
1-D sample of positions.
|
|
139
|
+
z_lo, z_hi : float
|
|
140
|
+
Bounding interval used for the pilot histogram.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
float
|
|
145
|
+
Bandwidth ``h``.
|
|
146
|
+
"""
|
|
147
|
+
z = np.asarray(z_data, dtype=float).ravel()
|
|
148
|
+
n = z.size
|
|
149
|
+
if n < 2:
|
|
150
|
+
return 0.1
|
|
151
|
+
h_silver = silverman_bw(z)
|
|
152
|
+
|
|
153
|
+
n_hist = 50
|
|
154
|
+
counts, edges = np.histogram(z, bins=n_hist, range=(z_lo, z_hi))
|
|
155
|
+
centers = 0.5 * (edges[:-1] + edges[1:])
|
|
156
|
+
bin_width = edges[1] - edges[0]
|
|
157
|
+
f_pilot_raw = counts.astype(float) / (n * bin_width)
|
|
158
|
+
|
|
159
|
+
n_fine = 400
|
|
160
|
+
z_fine = np.linspace(z_lo, z_hi, n_fine)
|
|
161
|
+
dz = z_fine[1] - z_fine[0]
|
|
162
|
+
h0 = max(h_silver, dz)
|
|
163
|
+
diff = z_fine[:, None] - centers[None, :]
|
|
164
|
+
s = diff / h0
|
|
165
|
+
kernel = np.where(np.abs(s) < 1.0, 0.75 * (1.0 - s * s) / h0, 0.0)
|
|
166
|
+
f_pilot = kernel @ f_pilot_raw
|
|
167
|
+
|
|
168
|
+
integral = np.trapezoid(f_pilot, z_fine)
|
|
169
|
+
if integral > 0:
|
|
170
|
+
f_pilot = f_pilot / integral
|
|
171
|
+
|
|
172
|
+
f_pp = np.empty_like(f_pilot)
|
|
173
|
+
f_pp[1:-1] = (f_pilot[2:] - 2.0 * f_pilot[1:-1] + f_pilot[:-2]) / (dz**2)
|
|
174
|
+
f_pp[0] = f_pp[1]
|
|
175
|
+
f_pp[-1] = f_pp[-2]
|
|
176
|
+
|
|
177
|
+
int_fpp_sq = np.trapezoid(f_pp**2, z_fine)
|
|
178
|
+
|
|
179
|
+
if int_fpp_sq <= 0 or not np.isfinite(int_fpp_sq):
|
|
180
|
+
return h_silver
|
|
181
|
+
h_sj = (_EPA_KERNEL_NORM_SQ / (n * _EPA_KERNEL_MU2_SQ * int_fpp_sq)) ** 0.2
|
|
182
|
+
h_sj *= 1.05
|
|
183
|
+
if not np.isfinite(h_sj) or h_sj <= 0 or h_sj > h_silver:
|
|
184
|
+
return h_silver
|
|
185
|
+
return float(h_sj)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def select_bandwidth(z_data, z_lo, z_hi, method="auto"):
|
|
189
|
+
"""Select the KDE bandwidth.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
z_data : array_like
|
|
194
|
+
1-D sample of positions.
|
|
195
|
+
z_lo, z_hi : float
|
|
196
|
+
Bounding interval (only used by the plug-in stage).
|
|
197
|
+
method : {"auto", "silverman"} or float
|
|
198
|
+
``"auto"`` uses :func:`sheather_jones_bw` with a
|
|
199
|
+
:func:`silverman_bw` fallback; ``"silverman"`` uses the rule of thumb
|
|
200
|
+
directly; a float fixes the bandwidth.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
float
|
|
205
|
+
Bandwidth ``h``.
|
|
206
|
+
"""
|
|
207
|
+
if method is None or method == "auto":
|
|
208
|
+
h = sheather_jones_bw(z_data, z_lo, z_hi)
|
|
209
|
+
if not np.isfinite(h) or h <= 0:
|
|
210
|
+
h = silverman_bw(z_data)
|
|
211
|
+
return float(h)
|
|
212
|
+
if method == "silverman":
|
|
213
|
+
return silverman_bw(z_data)
|
|
214
|
+
return float(method)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def epanechnikov_kernel(x, h):
|
|
218
|
+
r"""Epanechnikov kernel :math:`K_h(x) = \frac{3}{4h}(1 - (x/h)^2)` for
|
|
219
|
+
:math:`|x| < h`, zero otherwise.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
x : array_like
|
|
224
|
+
Offsets (any shape).
|
|
225
|
+
h : float
|
|
226
|
+
Bandwidth (must be positive).
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
numpy.ndarray
|
|
231
|
+
Kernel values, same shape as ``x``.
|
|
232
|
+
"""
|
|
233
|
+
s = np.asarray(x, dtype=float) / h
|
|
234
|
+
return np.where(np.abs(s) < 1.0, 0.75 * (1.0 - s * s) / h, 0.0)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def kde_1d(z_data, z_eval, h, z_bot, z_top, *, chunk_size=50_000):
|
|
238
|
+
r"""1-D Epanechnikov KDE with mirror-reflection boundary handling.
|
|
239
|
+
|
|
240
|
+
For each evaluation point :math:`z_0` in ``z_eval``,
|
|
241
|
+
|
|
242
|
+
.. math::
|
|
243
|
+
|
|
244
|
+
\hat\rho(z_0) = \frac{1}{N}\sum_j K_h(z_0 - z_j),
|
|
245
|
+
|
|
246
|
+
with mirror copies at :math:`2z_{\mathrm{bot}} - z_j` and
|
|
247
|
+
:math:`2z_{\mathrm{top}} - z_j` added to suppress leakage beyond the
|
|
248
|
+
boundaries. The :math:`O(N \cdot M)` kernel evaluation is performed in
|
|
249
|
+
chunks of ``chunk_size`` rows to keep the broadcasting array
|
|
250
|
+
memory-bounded.
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
z_data : array_like
|
|
255
|
+
``(N,)`` pooled sample positions.
|
|
256
|
+
z_eval : array_like
|
|
257
|
+
``(M,)`` evaluation grid points.
|
|
258
|
+
h : float
|
|
259
|
+
Bandwidth (must be positive).
|
|
260
|
+
z_bot, z_top : float
|
|
261
|
+
Mirror-reflection boundaries of the confined region.
|
|
262
|
+
chunk_size : int, optional
|
|
263
|
+
Row-chunk size for the kernel matrix. Default 50_000.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
rho_hat : numpy.ndarray
|
|
268
|
+
``(M,)`` normalised KDE density (1/length, integrates to ~1 over the
|
|
269
|
+
unbounded support).
|
|
270
|
+
n_eff : numpy.ndarray
|
|
271
|
+
``(M,)`` Kish effective sample size
|
|
272
|
+
:math:`(\sum_j K_h)^2 / \sum_j K_h^2`.
|
|
273
|
+
"""
|
|
274
|
+
z = np.asarray(z_data, dtype=float).ravel()
|
|
275
|
+
z_eval = np.asarray(z_eval, dtype=float).ravel()
|
|
276
|
+
n = z.size
|
|
277
|
+
M = z_eval.size
|
|
278
|
+
|
|
279
|
+
if h <= 0:
|
|
280
|
+
raise ValueError(f"bandwidth must be positive, got {h}")
|
|
281
|
+
|
|
282
|
+
if n == 0 or M == 0:
|
|
283
|
+
return np.zeros(M, dtype=np.float64), np.zeros(M, dtype=np.float64)
|
|
284
|
+
|
|
285
|
+
z_aug = np.concatenate([z, 2.0 * z_bot - z, 2.0 * z_top - z])
|
|
286
|
+
|
|
287
|
+
w_sum = np.zeros(M, dtype=np.float64)
|
|
288
|
+
w2_sum = np.zeros(M, dtype=np.float64)
|
|
289
|
+
|
|
290
|
+
n_aug = z_aug.size
|
|
291
|
+
for i0 in range(0, n_aug, chunk_size):
|
|
292
|
+
i1 = min(i0 + chunk_size, n_aug)
|
|
293
|
+
z_chunk = z_aug[i0:i1][:, None]
|
|
294
|
+
diff = z_eval[None, :] - z_chunk
|
|
295
|
+
w = epanechnikov_kernel(diff, h)
|
|
296
|
+
w_sum += w.sum(axis=0)
|
|
297
|
+
w2_sum += (w * w).sum(axis=0)
|
|
298
|
+
|
|
299
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
300
|
+
rho_hat = w_sum / n
|
|
301
|
+
n_eff = np.where(w2_sum > 0, w_sum**2 / w2_sum, 0.0)
|
|
302
|
+
return rho_hat, n_eff
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class TransverseDensityQKDE(AnalysisBase):
|
|
306
|
+
r"""Epanechnikov KDE transverse number-density profile.
|
|
307
|
+
|
|
308
|
+
Pools per-frame positions of ``atomgroup`` along ``dim`` (the confined
|
|
309
|
+
axis) across the analysis window and evaluates an Epanechnikov-kernel KDE
|
|
310
|
+
on a uniform grid spanning the confined region ``[z_bot, z_top]``. Kernel
|
|
311
|
+
mass leaking beyond the boundaries is folded back by mirror reflection.
|
|
312
|
+
|
|
313
|
+
Parameters
|
|
314
|
+
----------
|
|
315
|
+
atomgroup : MDAnalysis.core.groups.AtomGroup
|
|
316
|
+
Atoms whose positions are sampled (or, with ``grouping="residues"``,
|
|
317
|
+
the atoms whose residue centre-of-mass positions are sampled).
|
|
318
|
+
dim : int, optional
|
|
319
|
+
Confined-axis index (0=x, 1=y, 2=z). Default 2 (z).
|
|
320
|
+
z_bot, z_top : float, optional
|
|
321
|
+
Boundaries of the confined region used for mirror reflection and the
|
|
322
|
+
evaluation grid. If ``None`` they default to ``0`` and the box length
|
|
323
|
+
along ``dim`` (taken from the final analysis frame's
|
|
324
|
+
:attr:`~MDAnalysis.coordinates.timestep.Timestep.dimensions`).
|
|
325
|
+
n_points : int, optional
|
|
326
|
+
Number of evaluation grid points ``M``. Default 400.
|
|
327
|
+
bandwidth : {"auto", "silverman"} or float, optional
|
|
328
|
+
Bandwidth selection method (see :func:`select_bandwidth`).
|
|
329
|
+
Default ``"auto"``.
|
|
330
|
+
grouping : {"atoms", "residues"}, optional
|
|
331
|
+
``"atoms"`` samples every atom's position each frame;
|
|
332
|
+
``"residues"`` samples the mass-weighted centre-of-mass of each
|
|
333
|
+
residue in ``atomgroup`` each frame. Default ``"atoms"``.
|
|
334
|
+
chunk_size : int, optional
|
|
335
|
+
Row-chunk size for the kernel matrix to bound memory. Default
|
|
336
|
+
50_000.
|
|
337
|
+
|
|
338
|
+
Attributes
|
|
339
|
+
----------
|
|
340
|
+
z_eval : numpy.ndarray
|
|
341
|
+
``(M,)`` evaluation grid (length).
|
|
342
|
+
rho : numpy.ndarray
|
|
343
|
+
``(M,)`` normalised KDE density (1/length), integrating to ~1 over the
|
|
344
|
+
unbounded support. Multiply by the mean per-frame 2-D number density
|
|
345
|
+
``n_total / (n_frames_used * A)`` (where ``A`` is the cross-sectional
|
|
346
|
+
area) to obtain a number density in particles/volume.
|
|
347
|
+
n_eff : numpy.ndarray
|
|
348
|
+
``(M,)`` Kish effective sample size at each grid point.
|
|
349
|
+
bandwidth : float
|
|
350
|
+
Bandwidth used (length).
|
|
351
|
+
n_total : int
|
|
352
|
+
Total number of pooled samples.
|
|
353
|
+
n_frames_used : int
|
|
354
|
+
Number of analysis frames pooled.
|
|
355
|
+
n_per_frame : float
|
|
356
|
+
Mean number of samples per frame (``n_total / n_frames_used``).
|
|
357
|
+
z_pooled : numpy.ndarray
|
|
358
|
+
``(n_total,)`` pooled sample positions (handy for diagnostics).
|
|
359
|
+
|
|
360
|
+
Notes
|
|
361
|
+
-----
|
|
362
|
+
Confined-axis positions are wrapped into the primary box cell along
|
|
363
|
+
``dim`` before pooling, matching the behaviour of the original project
|
|
364
|
+
script. For ``grouping="residues"`` the centre-of-mass is computed with
|
|
365
|
+
the atoms' topology masses; ensure masses are present in the topology for
|
|
366
|
+
a true centre-of-mass (they default to 1.0, giving the geometric
|
|
367
|
+
centroid).
|
|
368
|
+
|
|
369
|
+
Examples
|
|
370
|
+
--------
|
|
371
|
+
::
|
|
372
|
+
|
|
373
|
+
import MDAnalysis as mda
|
|
374
|
+
from qdiffusivity import TransverseDensityQKDE
|
|
375
|
+
|
|
376
|
+
u = mda.Universe("topology.data", "trajectory.xtc")
|
|
377
|
+
ag = u.select_atoms("type 1 2")
|
|
378
|
+
kde = TransverseDensityQKDE(
|
|
379
|
+
ag, dim=2, z_bot=10.0, z_top=90.0, grouping="residues",
|
|
380
|
+
)
|
|
381
|
+
kde.run()
|
|
382
|
+
# number density (particles/Å^3): N_total / (n_frames * Lx * Ly) * rho
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
def __init__(
|
|
386
|
+
self,
|
|
387
|
+
atomgroup,
|
|
388
|
+
*,
|
|
389
|
+
dim: int = 2,
|
|
390
|
+
z_bot=None,
|
|
391
|
+
z_top=None,
|
|
392
|
+
n_points: int = 400,
|
|
393
|
+
bandwidth="auto",
|
|
394
|
+
grouping: str = "atoms",
|
|
395
|
+
chunk_size: int = 50_000,
|
|
396
|
+
):
|
|
397
|
+
super().__init__(atomgroup.universe.trajectory)
|
|
398
|
+
self._ag = atomgroup
|
|
399
|
+
if dim not in (0, 1, 2):
|
|
400
|
+
raise ValueError(f"dim must be 0, 1 or 2, got {dim}")
|
|
401
|
+
self._dim = dim
|
|
402
|
+
self._z_bot_user = z_bot
|
|
403
|
+
self._z_top_user = z_top
|
|
404
|
+
self._n_points = int(n_points)
|
|
405
|
+
if grouping not in ("atoms", "residues"):
|
|
406
|
+
raise ValueError(
|
|
407
|
+
f"grouping must be 'atoms' or 'residues', got {grouping!r}"
|
|
408
|
+
)
|
|
409
|
+
self._grouping = grouping
|
|
410
|
+
if isinstance(bandwidth, str) and bandwidth not in (
|
|
411
|
+
"auto", "silverman"
|
|
412
|
+
):
|
|
413
|
+
raise ValueError(
|
|
414
|
+
f"bandwidth must be 'auto', 'silverman' or a float, "
|
|
415
|
+
f"got {bandwidth!r}"
|
|
416
|
+
)
|
|
417
|
+
self._bandwidth = bandwidth
|
|
418
|
+
self._chunk_size = int(chunk_size)
|
|
419
|
+
|
|
420
|
+
def _prepare(self):
|
|
421
|
+
if self._grouping == "residues":
|
|
422
|
+
residx = self._ag.resindices
|
|
423
|
+
self._res_unique, self._res_inv = np.unique(
|
|
424
|
+
residx, return_inverse=True
|
|
425
|
+
)
|
|
426
|
+
self._n_res = self._res_unique.size
|
|
427
|
+
self._masses = self._ag.masses.astype(np.float64)
|
|
428
|
+
self._res_mass = np.zeros(self._n_res, dtype=np.float64)
|
|
429
|
+
np.add.at(self._res_mass, self._res_inv, self._masses)
|
|
430
|
+
self._z_frames = []
|
|
431
|
+
|
|
432
|
+
def _single_frame(self):
|
|
433
|
+
L = float(self._ts.dimensions[self._dim])
|
|
434
|
+
if self._grouping == "residues":
|
|
435
|
+
z_atoms = self._ag.positions[:, self._dim]
|
|
436
|
+
masses = self._masses
|
|
437
|
+
mz = np.zeros(self._n_res, dtype=np.float64)
|
|
438
|
+
np.add.at(mz, self._res_inv, masses * z_atoms)
|
|
439
|
+
com_z = mz / self._res_mass
|
|
440
|
+
if L > 0:
|
|
441
|
+
com_z %= L
|
|
442
|
+
self._z_frames.append(com_z)
|
|
443
|
+
else:
|
|
444
|
+
z = self._ag.positions[:, self._dim].astype(np.float64).copy()
|
|
445
|
+
if L > 0:
|
|
446
|
+
z %= L
|
|
447
|
+
self._z_frames.append(z)
|
|
448
|
+
|
|
449
|
+
def _conclude(self):
|
|
450
|
+
z_pooled = (
|
|
451
|
+
np.concatenate(self._z_frames)
|
|
452
|
+
if self._z_frames
|
|
453
|
+
else np.empty(0, dtype=np.float64)
|
|
454
|
+
)
|
|
455
|
+
self.z_pooled = z_pooled
|
|
456
|
+
self.n_total = int(z_pooled.size)
|
|
457
|
+
self.n_frames_used = len(self._z_frames)
|
|
458
|
+
self.n_per_frame = (
|
|
459
|
+
self.n_total / self.n_frames_used if self.n_frames_used > 0 else 0.0
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
L = float(self._ts.dimensions[self._dim])
|
|
463
|
+
z_bot = 0.0 if self._z_bot_user is None else float(self._z_bot_user)
|
|
464
|
+
z_top = L if self._z_top_user is None else float(self._z_top_user)
|
|
465
|
+
if z_top <= z_bot:
|
|
466
|
+
raise ValueError(
|
|
467
|
+
f"z_top ({z_top}) must be greater than z_bot ({z_bot})"
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
M = self._n_points
|
|
471
|
+
self.z_eval = z_bot + (np.arange(M) + 0.5) * (z_top - z_bot) / M
|
|
472
|
+
|
|
473
|
+
if z_pooled.size == 0:
|
|
474
|
+
self.rho = np.zeros(M, dtype=np.float64)
|
|
475
|
+
self.n_eff = np.zeros(M, dtype=np.float64)
|
|
476
|
+
self.bandwidth = float("nan")
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
self.bandwidth = select_bandwidth(
|
|
480
|
+
z_pooled, z_bot, z_top, method=self._bandwidth
|
|
481
|
+
)
|
|
482
|
+
self.rho, self.n_eff = kde_1d(
|
|
483
|
+
z_pooled,
|
|
484
|
+
self.z_eval,
|
|
485
|
+
self.bandwidth,
|
|
486
|
+
z_bot,
|
|
487
|
+
z_top,
|
|
488
|
+
chunk_size=self._chunk_size,
|
|
489
|
+
)
|