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.
@@ -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
@@ -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
+ )