3131 plot_extras ,
3232 plot_fitted_spectrum ,
3333)
34+ from utils import compute_msds
3435
3536# %%
3637mpl .rc_file ("matplotlibrc" )
7576ANGLES = np .arange (3 ) * ALPHA
7677EV = sc .value ("electron volt" ) / sc .value ("atomic unit of energy" )
7778AMPLITUDE = 0.2 * EV
79+ ANGSTROM = 1e-10 / sc .value ("atomic unit of length" )
7880
7981
8082def potential_energy_force (coords : ArrayLike ) -> tuple [NDArray , NDArray ]:
@@ -133,10 +135,10 @@ def plot_pes():
133135 ys = np .linspace (- 20 , 20 , 201 )
134136 coords = np .array (np .meshgrid (xs , ys )).transpose (1 , 2 , 0 )
135137 energies = potential_energy_force (coords )[0 ]
136- cf = ax .contourf (xs , ys , energies / EV , levels = 20 )
138+ cf = ax .contourf (xs / ANGSTROM , ys / ANGSTROM , energies / EV , levels = 20 )
137139 ax .set_aspect ("equal" , "box" )
138- ax .set_xlabel ("x [a$_0$ ]" )
139- ax .set_ylabel ("y [a$_0$ ]" )
140+ ax .set_xlabel ("x [Å ]" )
141+ ax .set_ylabel ("y [Å ]" )
140142 ax .set_title ("Potential Energy Surface" )
141143 fig .colorbar (cf , ax = ax , label = "Energy [eV]" )
142144
@@ -172,10 +174,13 @@ class Trajectory:
172174 """The spacing between two recorded time steps."""
173175
174176 coords : NDArray = attrs .field ()
175- """The time-dependent particle positions."""
177+ """The time-dependent particle positions, with shape `(natom, 2, nstep)`.
178+
179+ The last index is used for time steps, of which only every `block_size` step is recorded.
180+ """
176181
177182 vels : NDArray = attrs .field ()
178- """The time-dependent particle velocities.
183+ """The time-dependent particle velocities, with shape `(natom, 2, nstep)` .
179184
180185 If block_size is larger than 1,
181186 this attribute contains the block-averaged velocity.
@@ -192,16 +197,16 @@ def empty(cls, shape: tuple[int, ...], nstep: int, timestep: float):
192197 """Construct an empty trajectory object."""
193198 return cls (
194199 timestep ,
195- np .zeros ((nstep , * shape , 2 )),
196- np .zeros ((nstep , * shape , 2 )),
197- np .zeros ((nstep , * shape )),
198- np .zeros ((nstep , * shape )),
200+ np .zeros ((* shape , 2 , nstep )),
201+ np .zeros ((* shape , 2 , nstep )),
202+ np .zeros ((* shape , nstep )),
203+ np .zeros ((* shape , nstep )),
199204 )
200205
201206 @property
202207 def nstep (self ) -> int :
203208 """The number of time steps."""
204- return self .coords .shape [0 ]
209+ return self .coords .shape [- 1 ]
205210
206211
207212def integrate (coords : ArrayLike , vels : ArrayLike , nstep : int , block_size : int = 1 ):
@@ -239,10 +244,10 @@ def integrate(coords: ArrayLike, vels: ArrayLike, nstep: int, block_size: int =
239244 vels_block += vels
240245 if istep % block_size == block_size - 1 :
241246 itraj = istep // block_size
242- traj .coords [itraj ] = coords
243- traj .vels [itraj ] = vels_block / block_size
244- traj .potential_energies [itraj ] = energies
245- traj .kinetic_energies [itraj ] = (0.5 * MASS ) * (vels ** 2 ).sum (axis = - 1 )
247+ traj .coords [..., itraj ] = coords
248+ traj .vels [..., itraj ] = vels_block / block_size
249+ traj .potential_energies [..., itraj ] = energies
250+ traj .kinetic_energies [..., itraj ] = (0.5 * MASS ) * (vels ** 2 ).sum (axis = - 1 )
246251 vels_block = 0
247252 return traj
248253
@@ -266,11 +271,15 @@ def demo_energy_conservation():
266271 plt .close ("energy" )
267272 _ , ax = plt .subplots (num = "energy" )
268273 times = np .arange (traj .nstep ) * traj .timestep
269- ax .plot (times , traj .potential_energies , label = "potential" )
270- ax .plot (times , traj .potential_energies + traj .kinetic_energies , label = "total" )
274+ ax .plot (times / PICOSECOND , traj .potential_energies / EV , label = "potential" )
275+ ax .plot (
276+ times / PICOSECOND ,
277+ (traj .potential_energies + traj .kinetic_energies ) / EV ,
278+ label = "total" ,
279+ )
271280 ax .set_title ("Energy Conservation Demo" )
272- ax .set_xlabel ("Time [a.u. of time ]" )
273- ax .set_ylabel (r "Energy [E$_\mathrm{h}$ ]" )
281+ ax .set_xlabel ("Time [ps ]" )
282+ ax .set_ylabel ("Energy [eV ]" )
274283 ax .legend ()
275284
276285
@@ -296,26 +305,34 @@ def demo_chaos():
296305 plt .close ("chaos" )
297306 _ , ax = plt .subplots (num = "chaos" )
298307 ax .plot ([0 ], [0 ], "o" , color = "k" , label = "Initial position" )
299- ax .plot (traj .coords [:, 0 , 0 ], traj .coords [:, 0 , 1 ], color = "C1" , label = "Trajectory 1" )
300308 ax .plot (
301- traj .coords [:, 1 , 0 ],
302- traj .coords [:, 1 , 1 ],
309+ traj .coords [0 , 0 ] / ANGSTROM ,
310+ traj .coords [0 , 1 ] / ANGSTROM ,
311+ color = "C1" ,
312+ label = "Trajectory 1" ,
313+ )
314+ ax .plot (
315+ traj .coords [1 , 0 ] / ANGSTROM ,
316+ traj .coords [1 , 1 ] / ANGSTROM ,
303317 color = "C3" ,
304318 ls = ":" ,
305319 label = "Trajectory 2" ,
306320 )
307321 ax .set_aspect ("equal" , "box" )
308- ax .set_xlabel ("x [a$_0$ ]" )
309- ax .set_ylabel ("y [a$_0$ ]" )
322+ ax .set_xlabel ("x [Å ]" )
323+ ax .set_ylabel ("y [Å ]" )
310324 ax .legend ()
311325 ax .set_title ("Two Trajectories" )
312326
313327 plt .close ("chaos_dist" )
314328 _ , ax = plt .subplots (num = "chaos_dist" )
315329 times = np .arange (traj .nstep ) * traj .timestep
316- ax .semilogy (times , np .linalg .norm (traj .coords [:, 0 ] - traj .coords [:, 1 ], axis = - 1 ))
317- ax .set_xlabel ("Time [a.u. of time]" )
318- ax .set_ylabel ("Interparticle distance [a$_0$]" )
330+ ax .semilogy (
331+ times / PICOSECOND ,
332+ np .linalg .norm (traj .coords [0 ] - traj .coords [1 ], axis = 0 ) / ANGSTROM ,
333+ )
334+ ax .set_xlabel ("Time [ps]" )
335+ ax .set_ylabel ("Interparticle distance [Å]" )
319336 ax .set_title ("Slow Separation" )
320337
321338
@@ -367,14 +384,14 @@ def demo_stacie(block_size: int = 1):
367384 plt .close (f"trajs_{ block_size } " )
368385 _ , ax = plt .subplots (num = f"trajs_{ block_size } " , figsize = (6 , 6 ))
369386 for i in range (natom ):
370- ax .plot (traj .coords [:, i , 0 ], traj .coords [:, i , 1 ])
387+ ax .plot (traj .coords [i , 0 ], traj .coords [i , 1 ])
371388 ax .set_aspect ("equal" , "box" )
372389 ax .set_xlabel ("x [a$_0$]" )
373390 ax .set_ylabel ("y [a$_0$]" )
374391 ax .set_title (f"{ natom } Newtonian Pseudo-Random Walks" )
375392
376393 spectrum = compute_spectrum (
377- traj .vels .transpose ( 1 , 2 , 0 ). reshape (2 * natom , traj .nstep ),
394+ traj .vels .reshape (2 * natom , traj .nstep ),
378395 timestep = traj .timestep ,
379396 )
380397
@@ -408,11 +425,11 @@ def demo_stacie(block_size: int = 1):
408425 plt .close (f"extras_{ block_size } " )
409426 _ , axs = plt .subplots (2 , 2 , num = f"extras_{ block_size } " )
410427 plot_extras (axs , uc , result )
411- return result
428+ return traj , result
412429
413430
414431# %%
415- result_1 = demo_stacie ()
432+ traj_1 , result_1 = demo_stacie ()
416433
417434# %% [markdown]
418435# The spectrum has several peaks related to oscillations of the particles
@@ -464,7 +481,7 @@ def demo_stacie(block_size: int = 1):
464481# Let's use a block size of 60 to stay on the safe side.
465482
466483# %%
467- result_60 = demo_stacie (60 )
484+ traj_60 , result_60 = demo_stacie (60 )
468485
469486# %% [markdown]
470487# As expected, there are no significant changes in the results.
@@ -482,6 +499,53 @@ def demo_stacie(block_size: int = 1):
482499# Taking block averages removes the fastest oscillations,
483500# causing the integrated autocorrelation time to be dominated by slow diffusive motion.
484501
502+ # %% [markdown]
503+ # ## Comparison to mean-squared displacement analysis
504+ #
505+ # This section does not perform a full regression to derive the diffusion coefficient
506+ # from the mean-squared displacement (MSD) data.
507+ # Instead, it simply computes the MSDs from the trajectories,
508+ # and plots them together with the expected slope from STACIE's analysis above,
509+ # to confirm that STACIE's results are consistent with the MSD analysis.
510+ # This avoids the pernicious choices required for a full regression analysis of the MSD data.
511+
512+
513+ # %%
514+ def plot_msd (traj , result , lags , time_step ):
515+ # Integer time lags
516+ lag_times = lags * time_step
517+ natom = traj .coords .shape [0 ]
518+ msds = compute_msds (
519+ traj .coords .reshape (natom * 2 , traj .nstep ),
520+ lags ,
521+ )
522+ plt .close ("msd" )
523+ _ , ax = plt .subplots (num = "msd" )
524+ ax .plot (
525+ lag_times / PICOSECOND , msds / ANGSTROM ** 2 , "C0o" , label = "MSD from trajectories"
526+ )
527+ ax .plot (
528+ lag_times / PICOSECOND ,
529+ 2 * result .acint * lag_times / ANGSTROM ** 2 ,
530+ "C1-" ,
531+ label = "Expected slope" ,
532+ )
533+ ax .set_xlabel ("Lag time [ps]" )
534+ ax .set_ylabel ("Mean-squared displacement [Å$^2$]" )
535+
536+
537+ lags = np .unique (np .logspace (1 , 3 , 50 ).astype (int ))
538+ plot_msd (traj_1 , result_1 , lags , TIMESTEP )
539+
540+ # %% [markdown]
541+ # As expected, the simple comparison confirms that STACIE's results
542+ # are consistent with the MSD analysis.
543+ # For sufficiently large lag times, the MSDs increase linearly with time,
544+ # with a slope that corresponds to the diffusion coefficient derived with STACIE.
545+ #
546+ # Note that STACIE only estimates the slope of a straight line fitted to the MSD curve.
547+ # It does not provide information on the intercept.
548+
485549# %% [markdown]
486550# ## Regression Tests
487551#
0 commit comments