qdiffusivity 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/PKG-INFO +1 -1
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/__init__.py +8 -4
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/binned.py +190 -86
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/density.py +136 -50
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/diffusivity.py +103 -50
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/tests/conftest.py +9 -4
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/tests/test_binned.py +99 -25
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/tests/test_density.py +77 -29
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/tests/test_diffusivity.py +83 -13
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity.egg-info/PKG-INFO +1 -1
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/AUTHORS.md +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/CHANGELOG.md +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/CODE_OF_CONDUCT.md +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/LICENSE +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/MANIFEST.in +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/README.md +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/pyproject.toml +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/data/README.md +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/data/__init__.py +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/data/files.py +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/data/mda.txt +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/example_api.py +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/tests/__init__.py +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/tests/test_qdiffusivity.py +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity/tests/utils.py +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity.egg-info/SOURCES.txt +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity.egg-info/dependency_links.txt +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity.egg-info/requires.txt +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/qdiffusivity.egg-info/top_level.txt +0 -0
- {qdiffusivity-0.1.0 → qdiffusivity-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qdiffusivity
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Quantile-based estimation of local diffusivities in molecular dynamics simulations of nanoconfined liquids
|
|
5
5
|
Author-email: Shern Ren Tee <shernren@gmail.com>
|
|
6
6
|
Maintainer-email: Shern Ren Tee <shernren@gmail.com>
|
|
@@ -9,12 +9,14 @@ from importlib.metadata import version
|
|
|
9
9
|
|
|
10
10
|
from .binned import (
|
|
11
11
|
LocalDiffusivityQBinned,
|
|
12
|
-
|
|
12
|
+
TransverseMassDensityQBinned,
|
|
13
|
+
TransverseNumDensityQBinned,
|
|
13
14
|
cic_assign,
|
|
14
15
|
resolve_bins,
|
|
15
16
|
)
|
|
16
17
|
from .density import (
|
|
17
|
-
|
|
18
|
+
TransverseMassDensityQKDE,
|
|
19
|
+
TransverseNumDensityQKDE,
|
|
18
20
|
epanechnikov_kernel,
|
|
19
21
|
kde_1d,
|
|
20
22
|
select_bandwidth,
|
|
@@ -32,10 +34,12 @@ from .diffusivity import (
|
|
|
32
34
|
__version__ = version("qdiffusivity")
|
|
33
35
|
|
|
34
36
|
__all__ = [
|
|
35
|
-
"TransverseDensityQBinned",
|
|
36
|
-
"TransverseDensityQKDE",
|
|
37
37
|
"LocalDiffusivityQBinned",
|
|
38
38
|
"LocalDiffusivityQKDE",
|
|
39
|
+
"TransverseMassDensityQBinned",
|
|
40
|
+
"TransverseMassDensityQKDE",
|
|
41
|
+
"TransverseNumDensityQBinned",
|
|
42
|
+
"TransverseNumDensityQKDE",
|
|
39
43
|
"build_cdf",
|
|
40
44
|
"cic_assign",
|
|
41
45
|
"epanechnikov_kernel",
|
|
@@ -30,8 +30,8 @@ from __future__ import annotations
|
|
|
30
30
|
|
|
31
31
|
import numpy as np
|
|
32
32
|
from MDAnalysis.analysis.base import AnalysisBase
|
|
33
|
-
from MDAnalysis.transformations import NoJump
|
|
34
33
|
|
|
34
|
+
from .density import _AMU_PER_ANGSTROM3_TO_G_PER_CM3
|
|
35
35
|
from .diffusivity import build_cdf
|
|
36
36
|
|
|
37
37
|
|
|
@@ -138,14 +138,10 @@ def _bin_centers_from_edges(edges):
|
|
|
138
138
|
return 0.5 * (edges[:-1] + edges[1:])
|
|
139
139
|
|
|
140
140
|
|
|
141
|
-
class
|
|
142
|
-
r"""CDF-binned transverse
|
|
141
|
+
class _TransverseDensityQBinnedBase(AnalysisBase):
|
|
142
|
+
r"""Shared base for CDF-binned transverse density profiles.
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
confined axis), builds a CDF-uniformised u-space, and assigns each
|
|
146
|
-
sample to u-space bins via cloud-in-cell (CIC) or hard assignment.
|
|
147
|
-
The bin population is converted to a number density via the
|
|
148
|
-
cross-sectional area and the number of analysis frames.
|
|
144
|
+
Subclasses set ``_mass_density`` to select number or mass density.
|
|
149
145
|
|
|
150
146
|
Parameters
|
|
151
147
|
----------
|
|
@@ -175,15 +171,12 @@ class TransverseDensityQBinned(AnalysisBase):
|
|
|
175
171
|
u_centers : numpy.ndarray
|
|
176
172
|
``(n_bins,)`` bin centres in u-space.
|
|
177
173
|
density : numpy.ndarray
|
|
178
|
-
``(n_bins,)``
|
|
179
|
-
|
|
180
|
-
bin_width_in_u) * rho(z_center)`` — i.e. the CDF bin population
|
|
181
|
-
rescaled by the local Jacobian :math:`\rho = du/dz`.
|
|
174
|
+
``(n_bins,)`` density profile. For number density:
|
|
175
|
+
particles/ų. For mass density: g/cm³.
|
|
182
176
|
n_per_bin : numpy.ndarray
|
|
183
177
|
``(n_bins,)`` raw (weighted) population per bin.
|
|
184
178
|
n_eff : numpy.ndarray
|
|
185
|
-
``(n_bins,)`` effective sample size
|
|
186
|
-
integer count for hard assignment).
|
|
179
|
+
``(n_bins,)`` effective sample size.
|
|
187
180
|
bin_edges_u : numpy.ndarray
|
|
188
181
|
``(n_bins + 1,)`` u-space bin edges.
|
|
189
182
|
n_total : int
|
|
@@ -194,29 +187,10 @@ class TransverseDensityQBinned(AnalysisBase):
|
|
|
194
187
|
Mean number of samples per frame.
|
|
195
188
|
P, P_inv, rho : callables
|
|
196
189
|
CDF, inverse-CDF and equilibrium density closures.
|
|
197
|
-
|
|
198
|
-
Notes
|
|
199
|
-
-----
|
|
200
|
-
Confined-axis positions are wrapped into the primary box cell along
|
|
201
|
-
``dim`` before pooling. For ``grouping="residues"`` the
|
|
202
|
-
centre-of-mass is computed with the atoms' topology masses.
|
|
203
|
-
|
|
204
|
-
Examples
|
|
205
|
-
--------
|
|
206
|
-
::
|
|
207
|
-
|
|
208
|
-
import MDAnalysis as mda
|
|
209
|
-
from qdiffusivity import TransverseDensityQBinned
|
|
210
|
-
|
|
211
|
-
u = mda.Universe("topology.data", "trajectory.xtc")
|
|
212
|
-
ag = u.select_atoms("type 1 2")
|
|
213
|
-
binned = TransverseDensityQBinned(
|
|
214
|
-
ag, dim=2, z_bot=10.0, z_top=90.0, bins=30,
|
|
215
|
-
)
|
|
216
|
-
binned.run()
|
|
217
|
-
# binned.density is in particles/Å^3.
|
|
218
190
|
"""
|
|
219
191
|
|
|
192
|
+
_mass_density = False
|
|
193
|
+
|
|
220
194
|
def __init__(
|
|
221
195
|
self,
|
|
222
196
|
atomgroup,
|
|
@@ -242,13 +216,13 @@ class TransverseDensityQBinned(AnalysisBase):
|
|
|
242
216
|
self._grouping = grouping
|
|
243
217
|
|
|
244
218
|
def _prepare(self):
|
|
219
|
+
self._masses = self._ag.masses.astype(np.float64)
|
|
245
220
|
if self._grouping == "residues":
|
|
246
221
|
residx = self._ag.resindices
|
|
247
222
|
self._res_unique, self._res_inv = np.unique(
|
|
248
223
|
residx, return_inverse=True
|
|
249
224
|
)
|
|
250
225
|
self._n_res = self._res_unique.size
|
|
251
|
-
self._masses = self._ag.masses.astype(np.float64)
|
|
252
226
|
self._res_mass = np.zeros(self._n_res, dtype=np.float64)
|
|
253
227
|
np.add.at(self._res_mass, self._res_inv, self._masses)
|
|
254
228
|
self._u_frames = []
|
|
@@ -282,7 +256,14 @@ class TransverseDensityQBinned(AnalysisBase):
|
|
|
282
256
|
self.n_total / self.n_frames_used if self.n_frames_used > 0 else 0.0
|
|
283
257
|
)
|
|
284
258
|
|
|
285
|
-
|
|
259
|
+
(
|
|
260
|
+
self.P,
|
|
261
|
+
self.P_inv,
|
|
262
|
+
self.rho,
|
|
263
|
+
self.rho_prime,
|
|
264
|
+
_,
|
|
265
|
+
_,
|
|
266
|
+
) = build_cdf(z_frames)
|
|
286
267
|
self.u_centers = _bin_centers_from_edges(self._edges_u)
|
|
287
268
|
self.z_centers = self.P_inv(self.u_centers)
|
|
288
269
|
n_bins = self._n_bins
|
|
@@ -302,8 +283,6 @@ class TransverseDensityQBinned(AnalysisBase):
|
|
|
302
283
|
np.add.at(pop, k1, w1)
|
|
303
284
|
self.n_eff = pop.copy()
|
|
304
285
|
else:
|
|
305
|
-
# Hard assignment via digitize. Samples exactly on the
|
|
306
|
-
# upper edge (u == 1) are folded into the last bin.
|
|
307
286
|
idx = np.clip(
|
|
308
287
|
np.digitize(u_samples, self._edges_u) - 1,
|
|
309
288
|
0,
|
|
@@ -314,26 +293,109 @@ class TransverseDensityQBinned(AnalysisBase):
|
|
|
314
293
|
|
|
315
294
|
self.n_per_bin = pop
|
|
316
295
|
|
|
317
|
-
#
|
|
318
|
-
# n(z) = (N_total / (n_frames * A)) * rho(z)
|
|
319
|
-
# because in u-space the population per bin is ~N_total * du,
|
|
320
|
-
# and du = rho(z) * dz, so the z-space number density is
|
|
321
|
-
# (N_total / (n_frames * A)) * (pop / N_total) / dz
|
|
322
|
-
# = pop / (n_frames * A * dz), with dz = du / rho(z).
|
|
323
|
-
A = 1.0
|
|
296
|
+
# Cross-sectional area.
|
|
324
297
|
dims = self._ag.universe.dimensions
|
|
325
|
-
if
|
|
326
|
-
|
|
327
|
-
|
|
298
|
+
para = [i for i in (0, 1, 2) if i != self._dim]
|
|
299
|
+
A = float(dims[para[0]] * dims[para[1]])
|
|
300
|
+
|
|
328
301
|
rho_centers = self.rho(self.z_centers)
|
|
329
302
|
du = np.diff(self._edges_u)
|
|
330
303
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
331
304
|
dz = np.where(rho_centers > 0, du / rho_centers, 0.0)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
305
|
+
|
|
306
|
+
if self._mass_density:
|
|
307
|
+
# Mass-weighted population: each sample's CIC/hard
|
|
308
|
+
# weight is multiplied by its mass. For CIC we
|
|
309
|
+
# re-accumulate with mass weights; for hard assignment
|
|
310
|
+
# we use np.bincount with weights.
|
|
311
|
+
if self._use_cic:
|
|
312
|
+
mass_per_sample = (
|
|
313
|
+
np.tile(self._masses, self.n_frames_used)
|
|
314
|
+
if self._grouping == "atoms"
|
|
315
|
+
else np.repeat(self._res_mass, self.n_frames_used)
|
|
316
|
+
)
|
|
317
|
+
mass_pop = np.zeros(n_bins, dtype=np.float64)
|
|
318
|
+
np.add.at(mass_pop, k0, w0 * mass_per_sample)
|
|
319
|
+
np.add.at(mass_pop, k1, w1 * mass_per_sample)
|
|
320
|
+
else:
|
|
321
|
+
mass_per_sample = (
|
|
322
|
+
np.tile(self._masses, self.n_frames_used)
|
|
323
|
+
if self._grouping == "atoms"
|
|
324
|
+
else np.repeat(self._res_mass, self.n_frames_used)
|
|
325
|
+
)
|
|
326
|
+
mass_pop = np.bincount(
|
|
327
|
+
idx,
|
|
328
|
+
weights=mass_per_sample,
|
|
329
|
+
minlength=n_bins,
|
|
330
|
+
).astype(np.float64)
|
|
331
|
+
# Mass density (amu/Å^3) -> g/cm^3.
|
|
332
|
+
self.density = np.where(
|
|
333
|
+
dz > 0,
|
|
334
|
+
mass_pop
|
|
335
|
+
/ (self.n_frames_used * A * dz)
|
|
336
|
+
/ _AMU_PER_ANGSTROM3_TO_G_PER_CM3,
|
|
337
|
+
0.0,
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
self.density = np.where(
|
|
341
|
+
dz > 0,
|
|
342
|
+
pop / (self.n_frames_used * A * dz),
|
|
343
|
+
0.0,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class TransverseNumDensityQBinned(_TransverseDensityQBinnedBase):
|
|
348
|
+
r"""CDF-binned transverse number-density profile.
|
|
349
|
+
|
|
350
|
+
Returns number density in particles/ų. See
|
|
351
|
+
:class:`_TransverseDensityQBinnedBase` for parameters and attributes.
|
|
352
|
+
|
|
353
|
+
Examples
|
|
354
|
+
--------
|
|
355
|
+
::
|
|
356
|
+
|
|
357
|
+
import MDAnalysis as mda
|
|
358
|
+
from qdiffusivity import TransverseNumDensityQBinned
|
|
359
|
+
|
|
360
|
+
u = mda.Universe("topology.data", "trajectory.xtc")
|
|
361
|
+
ag = u.select_atoms("type 1 2")
|
|
362
|
+
binned = TransverseNumDensityQBinned(
|
|
363
|
+
ag, dim=2, z_bot=10.0, z_top=90.0, bins=30,
|
|
364
|
+
)
|
|
365
|
+
binned.run()
|
|
366
|
+
# binned.density is in particles/Å^3.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
_mass_density = False
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class TransverseMassDensityQBinned(_TransverseDensityQBinnedBase):
|
|
373
|
+
r"""CDF-binned transverse mass-density profile.
|
|
374
|
+
|
|
375
|
+
Returns mass density in g/cm³ (matching MDAnalysis's
|
|
376
|
+
:class:`~MDAnalysis.analysis.lineardensity.LinearDensity`). Each
|
|
377
|
+
sample is weighted by its topology mass (per atom for
|
|
378
|
+
``grouping="atoms"``; per-residue total mass for
|
|
379
|
+
``grouping="residues"``). See
|
|
380
|
+
:class:`_TransverseDensityQBinnedBase` for parameters and attributes.
|
|
381
|
+
|
|
382
|
+
Examples
|
|
383
|
+
--------
|
|
384
|
+
::
|
|
385
|
+
|
|
386
|
+
import MDAnalysis as mda
|
|
387
|
+
from qdiffusivity import TransverseMassDensityQBinned
|
|
388
|
+
|
|
389
|
+
u = mda.Universe("topology.data", "trajectory.xtc")
|
|
390
|
+
ag = u.select_atoms("type 1 2")
|
|
391
|
+
binned = TransverseMassDensityQBinned(
|
|
392
|
+
ag, dim=2, z_bot=10.0, z_top=90.0, bins=30,
|
|
393
|
+
)
|
|
394
|
+
binned.run()
|
|
395
|
+
# binned.density is in g/cm^3.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
_mass_density = True
|
|
337
399
|
|
|
338
400
|
|
|
339
401
|
class LocalDiffusivityQBinned(AnalysisBase):
|
|
@@ -373,6 +435,17 @@ class LocalDiffusivityQBinned(AnalysisBase):
|
|
|
373
435
|
:math:`\frac{\Delta t}{2}\Phi(z)^2` (with
|
|
374
436
|
:math:`\Phi = D\,\rho'/\rho` in the isothermal convention) from
|
|
375
437
|
the perpendicular estimator. Default ``False``.
|
|
438
|
+
density_result : TransverseNumDensityQKDE or None, optional
|
|
439
|
+
A pre-computed density result whose CDF closures
|
|
440
|
+
(:attr:`P`, :attr:`P_inv`, :attr:`rho`, :attr:`rho_prime`)
|
|
441
|
+
are reused for the u-space mapping. If ``None`` (default),
|
|
442
|
+
a :class:`TransverseNumDensityQKDE` is run internally in
|
|
443
|
+
``_prepare()`` to build the CDF automatically. Passing a
|
|
444
|
+
pre-computed result enables a **two-pass parallelization**
|
|
445
|
+
strategy: run the density analysis first (parallelizable via
|
|
446
|
+
split-apply-combine), then pass its result to the diffusivity
|
|
447
|
+
analysis (each frame's u-mapping is then stateless and
|
|
448
|
+
parallelizable).
|
|
376
449
|
|
|
377
450
|
Attributes
|
|
378
451
|
----------
|
|
@@ -407,14 +480,18 @@ class LocalDiffusivityQBinned(AnalysisBase):
|
|
|
407
480
|
Time step used in the estimator.
|
|
408
481
|
P, P_inv, rho : callables
|
|
409
482
|
CDF, inverse-CDF and equilibrium density closures.
|
|
483
|
+
rho_prime : callable
|
|
484
|
+
Spatial derivative of the equilibrium density.
|
|
410
485
|
|
|
411
486
|
Notes
|
|
412
487
|
-----
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
488
|
+
Multi-frame displacements across periodic boundaries are computed
|
|
489
|
+
with the **minimum-image convention** applied directly to the
|
|
490
|
+
stored wrapped positions. This is a stateless, frame-local
|
|
491
|
+
operation — only consecutive-frame positions are needed — so the
|
|
492
|
+
estimator is compatible with split-apply-combine parallelization
|
|
493
|
+
(each worker block can be processed independently without missing
|
|
494
|
+
prior image corrections).
|
|
418
495
|
|
|
419
496
|
Examples
|
|
420
497
|
--------
|
|
@@ -439,6 +516,7 @@ class LocalDiffusivityQBinned(AnalysisBase):
|
|
|
439
516
|
bins=30,
|
|
440
517
|
dt=None,
|
|
441
518
|
ito_correction: bool = False,
|
|
519
|
+
density_result=None,
|
|
442
520
|
):
|
|
443
521
|
super().__init__(atomgroup.universe.trajectory)
|
|
444
522
|
self._ag = atomgroup
|
|
@@ -449,8 +527,26 @@ class LocalDiffusivityQBinned(AnalysisBase):
|
|
|
449
527
|
self._n_bins, self._edges_u, self._use_cic = resolve_bins(bins)
|
|
450
528
|
self._dt = dt
|
|
451
529
|
self._ito_correction = bool(ito_correction)
|
|
530
|
+
self._density_result = density_result
|
|
452
531
|
|
|
453
532
|
def _prepare(self):
|
|
533
|
+
if self._density_result is not None:
|
|
534
|
+
dr = self._density_result
|
|
535
|
+
self.P = dr.P
|
|
536
|
+
self.P_inv = dr.P_inv
|
|
537
|
+
self.rho = dr.rho
|
|
538
|
+
self.rho_prime = dr.rho_prime
|
|
539
|
+
else:
|
|
540
|
+
dens = TransverseNumDensityQBinned(
|
|
541
|
+
self._ag,
|
|
542
|
+
dim=self._dim,
|
|
543
|
+
bins=self._n_bins,
|
|
544
|
+
)
|
|
545
|
+
dens.run()
|
|
546
|
+
self.P = dens.P
|
|
547
|
+
self.P_inv = dens.P_inv
|
|
548
|
+
self.rho = dens.rho
|
|
549
|
+
self.rho_prime = dens.rho_prime
|
|
454
550
|
self._pos_frames = []
|
|
455
551
|
|
|
456
552
|
def _single_frame(self):
|
|
@@ -470,40 +566,44 @@ class LocalDiffusivityQBinned(AnalysisBase):
|
|
|
470
566
|
if dt <= 0:
|
|
471
567
|
raise ValueError(f"dt must be positive, got {dt}")
|
|
472
568
|
|
|
473
|
-
#
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
Lz
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
569
|
+
# Box lengths from the last analysed frame (constant-box / NVT
|
|
570
|
+
# assumption). Minimum-image correction is applied to the
|
|
571
|
+
# *displacement* of consecutive frames, not to the absolute
|
|
572
|
+
# positions — stateless and parallelization-friendly.
|
|
573
|
+
box = self._ts.dimensions
|
|
574
|
+
Lz = float(box[self._dim])
|
|
575
|
+
Lpara = {d: float(box[d]) for d in self._para_dims}
|
|
576
|
+
|
|
577
|
+
# Wrap the confined coordinate into [0, Lz] for CDF construction.
|
|
578
|
+
if Lz > 0:
|
|
579
|
+
pos[:, :, self._dim] %= Lz
|
|
580
|
+
|
|
581
|
+
# Build CDF from pooled wrapped-z — unless a pre-computed
|
|
582
|
+
# CDF was supplied (or auto-run in _prepare).
|
|
583
|
+
if self._density_result is None:
|
|
584
|
+
z_pooled = pos[:, :, self._dim].ravel()
|
|
585
|
+
(
|
|
586
|
+
self.P,
|
|
587
|
+
self.P_inv,
|
|
588
|
+
self.rho,
|
|
589
|
+
self.rho_prime,
|
|
590
|
+
_,
|
|
591
|
+
_,
|
|
592
|
+
) = build_cdf(z_pooled)
|
|
496
593
|
|
|
497
594
|
self.u_centers = _bin_centers_from_edges(self._edges_u)
|
|
498
595
|
self.z_centers = self.P_inv(self.u_centers)
|
|
499
596
|
n_bins = self._n_bins
|
|
500
597
|
|
|
501
598
|
# u-positions of all increments (starting frame of each pair).
|
|
502
|
-
z_start =
|
|
599
|
+
z_start = pos[:-1, :, self._dim]
|
|
503
600
|
u_start = self.P(z_start.ravel())
|
|
504
601
|
|
|
505
602
|
# Perpendicular: z-space local estimator (Δz)²/(2Δt).
|
|
506
|
-
|
|
603
|
+
# Minimum-image correction on the wrapped-z displacement.
|
|
604
|
+
dz = np.diff(pos[:, :, self._dim], axis=0)
|
|
605
|
+
if Lz > 0:
|
|
606
|
+
dz -= np.rint(dz / Lz) * Lz
|
|
507
607
|
d_perp_local = (dz**2) / (2.0 * dt)
|
|
508
608
|
self.n_increments = int(d_perp_local.size)
|
|
509
609
|
|
|
@@ -532,10 +632,14 @@ class LocalDiffusivityQBinned(AnalysisBase):
|
|
|
532
632
|
|
|
533
633
|
self.D_perp = np.clip(self.D_perp, 0, None)
|
|
534
634
|
|
|
535
|
-
# Parallel: Δx² + Δy², only z_start mapped to u.
|
|
635
|
+
# Parallel: Δx² + Δy², only z_start mapped to u. Minimum-image
|
|
636
|
+
# correction on each parallel displacement.
|
|
536
637
|
d_para_acc = np.zeros_like(dz)
|
|
537
638
|
for d in self._para_dims:
|
|
538
|
-
diff_d = np.diff(
|
|
639
|
+
diff_d = np.diff(pos[:, :, d], axis=0)
|
|
640
|
+
Ld = Lpara[d]
|
|
641
|
+
if Ld > 0:
|
|
642
|
+
diff_d -= np.rint(diff_d / Ld) * Ld
|
|
539
643
|
d_para_acc += diff_d**2
|
|
540
644
|
d_para_local = d_para_acc / (4.0 * dt)
|
|
541
645
|
|