crisp-ase 1.1.2__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.
- CRISP/__init__.py +99 -0
- CRISP/_version.py +1 -0
- CRISP/cli.py +41 -0
- CRISP/data_analysis/__init__.py +38 -0
- CRISP/data_analysis/clustering.py +838 -0
- CRISP/data_analysis/contact_coordination.py +915 -0
- CRISP/data_analysis/h_bond.py +772 -0
- CRISP/data_analysis/msd.py +1199 -0
- CRISP/data_analysis/prdf.py +404 -0
- CRISP/data_analysis/volumetric_atomic_density.py +527 -0
- CRISP/py.typed +1 -0
- CRISP/simulation_utility/__init__.py +31 -0
- CRISP/simulation_utility/atomic_indices.py +155 -0
- CRISP/simulation_utility/atomic_traj_linemap.py +278 -0
- CRISP/simulation_utility/error_analysis.py +254 -0
- CRISP/simulation_utility/interatomic_distances.py +200 -0
- CRISP/simulation_utility/subsampling.py +241 -0
- CRISP/tests/DataAnalysis/__init__.py +1 -0
- CRISP/tests/DataAnalysis/test_clustering_extended.py +212 -0
- CRISP/tests/DataAnalysis/test_contact_coordination.py +184 -0
- CRISP/tests/DataAnalysis/test_contact_coordination_extended.py +465 -0
- CRISP/tests/DataAnalysis/test_h_bond_complete.py +326 -0
- CRISP/tests/DataAnalysis/test_h_bond_extended.py +322 -0
- CRISP/tests/DataAnalysis/test_msd_complete.py +305 -0
- CRISP/tests/DataAnalysis/test_msd_extended.py +522 -0
- CRISP/tests/DataAnalysis/test_prdf.py +206 -0
- CRISP/tests/DataAnalysis/test_volumetric_atomic_density.py +463 -0
- CRISP/tests/SimulationUtility/__init__.py +1 -0
- CRISP/tests/SimulationUtility/test_atomic_traj_linemap.py +101 -0
- CRISP/tests/SimulationUtility/test_atomic_traj_linemap_extended.py +469 -0
- CRISP/tests/SimulationUtility/test_error_analysis_extended.py +151 -0
- CRISP/tests/SimulationUtility/test_interatomic_distances.py +223 -0
- CRISP/tests/SimulationUtility/test_subsampling.py +365 -0
- CRISP/tests/__init__.py +1 -0
- CRISP/tests/test_CRISP.py +28 -0
- CRISP/tests/test_cli.py +87 -0
- CRISP/tests/test_crisp_comprehensive.py +679 -0
- crisp_ase-1.1.2.dist-info/METADATA +141 -0
- crisp_ase-1.1.2.dist-info/RECORD +42 -0
- crisp_ase-1.1.2.dist-info/WHEEL +5 -0
- crisp_ase-1.1.2.dist-info/entry_points.txt +2 -0
- crisp_ase-1.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""Extended comprehensive tests for Mean Square Displacement (MSD) module."""
|
|
2
|
+
import pytest
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import shutil
|
|
8
|
+
from ase import Atoms
|
|
9
|
+
from ase.io import write, read
|
|
10
|
+
from CRISP.data_analysis.msd import (
|
|
11
|
+
calculate_msd,
|
|
12
|
+
calculate_frame_msd,
|
|
13
|
+
read_trajectory_chunk,
|
|
14
|
+
save_msd_data,
|
|
15
|
+
plot_diffusion_time_series,
|
|
16
|
+
block_averaging_error,
|
|
17
|
+
calculate_msd_windowed,
|
|
18
|
+
calculate_frame_msd_windowed,
|
|
19
|
+
msd_analysis,
|
|
20
|
+
calculate_diffusion_coefficient,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestCalculateFrameMSD:
|
|
25
|
+
"""Test single frame MSD calculation."""
|
|
26
|
+
|
|
27
|
+
def test_calculate_frame_msd_basic(self):
|
|
28
|
+
"""Test basic frame MSD calculation."""
|
|
29
|
+
ref_frame = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
30
|
+
curr_frame = Atoms('H2O', positions=[[0.1, 0, 0], [1.1, 0, 0], [0.1, 1, 0]])
|
|
31
|
+
atom_indices = np.array([0, 1, 2])
|
|
32
|
+
|
|
33
|
+
frame_idx, msd = calculate_frame_msd(0, curr_frame, ref_frame, atom_indices, msd_direction=False)
|
|
34
|
+
|
|
35
|
+
assert frame_idx == 0
|
|
36
|
+
assert isinstance(msd, (float, np.floating))
|
|
37
|
+
assert msd > 0
|
|
38
|
+
|
|
39
|
+
def test_calculate_frame_msd_directional(self):
|
|
40
|
+
"""Test directional MSD calculation."""
|
|
41
|
+
ref_frame = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
42
|
+
curr_frame = Atoms('H2O', positions=[[0.1, 0.2, 0.3], [1.1, 0, 0], [0.1, 1, 0]])
|
|
43
|
+
atom_indices = np.array([0, 1, 2])
|
|
44
|
+
|
|
45
|
+
result = calculate_frame_msd(1, curr_frame, ref_frame, atom_indices, msd_direction=True)
|
|
46
|
+
|
|
47
|
+
assert len(result) == 4 # frame_idx, msd_x, msd_y, msd_z
|
|
48
|
+
frame_idx, msd_x, msd_y, msd_z = result
|
|
49
|
+
assert frame_idx == 1
|
|
50
|
+
assert isinstance(msd_x, (float, np.floating))
|
|
51
|
+
assert isinstance(msd_y, (float, np.floating))
|
|
52
|
+
assert isinstance(msd_z, (float, np.floating))
|
|
53
|
+
assert all(v >= 0 for v in [msd_x, msd_y, msd_z])
|
|
54
|
+
|
|
55
|
+
def test_calculate_frame_msd_single_atom(self):
|
|
56
|
+
"""Test MSD for single atom."""
|
|
57
|
+
ref_frame = Atoms('H', positions=[[0, 0, 0]])
|
|
58
|
+
curr_frame = Atoms('H', positions=[[1, 1, 1]])
|
|
59
|
+
atom_indices = np.array([0])
|
|
60
|
+
|
|
61
|
+
frame_idx, msd = calculate_frame_msd(0, curr_frame, ref_frame, atom_indices)
|
|
62
|
+
|
|
63
|
+
assert msd == pytest.approx(3.0) # (1^2 + 1^2 + 1^2) / 1
|
|
64
|
+
|
|
65
|
+
def test_calculate_frame_msd_no_displacement(self):
|
|
66
|
+
"""Test MSD when atoms don't move."""
|
|
67
|
+
frame = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
68
|
+
atom_indices = np.array([0, 1, 2])
|
|
69
|
+
|
|
70
|
+
frame_idx, msd = calculate_frame_msd(0, frame, frame, atom_indices)
|
|
71
|
+
|
|
72
|
+
assert msd == pytest.approx(0.0)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestReadTrajectoryChunk:
|
|
76
|
+
"""Test trajectory file reading."""
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def sample_trajectory(self):
|
|
80
|
+
"""Create a sample trajectory file."""
|
|
81
|
+
temp_dir = tempfile.mkdtemp()
|
|
82
|
+
traj_path = os.path.join(temp_dir, 'test_traj.traj')
|
|
83
|
+
|
|
84
|
+
# Create multi-frame trajectory
|
|
85
|
+
frames = []
|
|
86
|
+
for i in range(5):
|
|
87
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
88
|
+
atoms.positions += np.random.rand(3, 3) * 0.1 * i
|
|
89
|
+
frames.append(atoms)
|
|
90
|
+
|
|
91
|
+
write(traj_path, frames)
|
|
92
|
+
yield traj_path
|
|
93
|
+
shutil.rmtree(temp_dir)
|
|
94
|
+
|
|
95
|
+
def test_read_trajectory_chunk_all_frames(self, sample_trajectory):
|
|
96
|
+
"""Test reading all frames."""
|
|
97
|
+
frames = read_trajectory_chunk(sample_trajectory, ':')
|
|
98
|
+
|
|
99
|
+
assert isinstance(frames, list)
|
|
100
|
+
assert len(frames) == 5
|
|
101
|
+
assert all(isinstance(f, Atoms) for f in frames)
|
|
102
|
+
|
|
103
|
+
def test_read_trajectory_chunk_specific_slice(self, sample_trajectory):
|
|
104
|
+
"""Test reading specific frame slice."""
|
|
105
|
+
frames = read_trajectory_chunk(sample_trajectory, '0:3')
|
|
106
|
+
|
|
107
|
+
assert len(frames) == 3
|
|
108
|
+
|
|
109
|
+
def test_read_trajectory_chunk_with_frame_skip(self, sample_trajectory):
|
|
110
|
+
"""Test reading with frame skip."""
|
|
111
|
+
frames = read_trajectory_chunk(sample_trajectory, ':', frame_skip=2)
|
|
112
|
+
|
|
113
|
+
# 5 frames with skip=2 should give 3 frames (indices 0, 2, 4)
|
|
114
|
+
assert len(frames) == 3
|
|
115
|
+
|
|
116
|
+
def test_read_trajectory_chunk_invalid_file(self):
|
|
117
|
+
"""Test reading non-existent file."""
|
|
118
|
+
result = read_trajectory_chunk('/nonexistent/path.traj', ':')
|
|
119
|
+
|
|
120
|
+
assert result == []
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestCalculateMSD:
|
|
124
|
+
"""Test MSD calculation for trajectories."""
|
|
125
|
+
|
|
126
|
+
@pytest.fixture
|
|
127
|
+
def trajectory(self):
|
|
128
|
+
"""Create sample trajectory."""
|
|
129
|
+
frames = []
|
|
130
|
+
for i in range(10):
|
|
131
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
132
|
+
# Linear displacement
|
|
133
|
+
atoms.positions += np.array([0.01*i, 0.01*i, 0])
|
|
134
|
+
frames.append(atoms)
|
|
135
|
+
return frames
|
|
136
|
+
|
|
137
|
+
def test_calculate_msd_basic(self, trajectory):
|
|
138
|
+
"""Test basic MSD calculation."""
|
|
139
|
+
timestep = 1.0
|
|
140
|
+
atom_indices = np.array([0, 1, 2])
|
|
141
|
+
|
|
142
|
+
result = calculate_msd(trajectory, timestep, atom_indices=atom_indices)
|
|
143
|
+
|
|
144
|
+
assert isinstance(result, tuple)
|
|
145
|
+
assert len(result) == 2 # (msd_values, msd_times)
|
|
146
|
+
msd_values, msd_times = result
|
|
147
|
+
assert len(msd_values) > 0
|
|
148
|
+
assert len(msd_times) > 0
|
|
149
|
+
assert msd_times[0] == 0.0
|
|
150
|
+
|
|
151
|
+
def test_calculate_msd_directional(self, trajectory):
|
|
152
|
+
"""Test directional MSD calculation."""
|
|
153
|
+
timestep = 1.0
|
|
154
|
+
atom_indices = np.array([0, 1, 2])
|
|
155
|
+
|
|
156
|
+
result = calculate_msd(trajectory, timestep, atom_indices=atom_indices, msd_direction=True)
|
|
157
|
+
|
|
158
|
+
assert isinstance(result, tuple)
|
|
159
|
+
assert len(result) == 4 # (msd_times, msd_x, msd_y, msd_z)
|
|
160
|
+
msd_times, msd_x, msd_y, msd_z = result
|
|
161
|
+
assert all(len(arr) > 0 for arr in [msd_times, msd_x, msd_y, msd_z])
|
|
162
|
+
|
|
163
|
+
def test_calculate_msd_with_ignore_frames(self, trajectory):
|
|
164
|
+
"""Test MSD calculation ignoring initial frames."""
|
|
165
|
+
timestep = 1.0
|
|
166
|
+
atom_indices = np.array([0, 1, 2])
|
|
167
|
+
|
|
168
|
+
result_full = calculate_msd(trajectory, timestep, atom_indices=atom_indices, ignore_n_images=0)
|
|
169
|
+
result_skip = calculate_msd(trajectory, timestep, atom_indices=atom_indices, ignore_n_images=3)
|
|
170
|
+
|
|
171
|
+
msd_full, time_full = result_full
|
|
172
|
+
msd_skip, time_skip = result_skip
|
|
173
|
+
|
|
174
|
+
assert len(time_skip) < len(time_full)
|
|
175
|
+
|
|
176
|
+
def test_calculate_msd_single_atom_index(self, trajectory):
|
|
177
|
+
"""Test MSD for single atom."""
|
|
178
|
+
timestep = 1.0
|
|
179
|
+
atom_indices = np.array([0])
|
|
180
|
+
|
|
181
|
+
msd_values, msd_times = calculate_msd(trajectory, timestep, atom_indices=atom_indices)
|
|
182
|
+
|
|
183
|
+
assert isinstance(msd_values, np.ndarray)
|
|
184
|
+
assert len(msd_values) > 0
|
|
185
|
+
# MSD should increase with time for linearly moving particle
|
|
186
|
+
assert msd_values[-1] > msd_values[0]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestSaveMSDData:
|
|
190
|
+
"""Test MSD data saving."""
|
|
191
|
+
|
|
192
|
+
def test_save_msd_data_basic(self):
|
|
193
|
+
"""Test basic MSD data saving."""
|
|
194
|
+
temp_dir = tempfile.mkdtemp()
|
|
195
|
+
|
|
196
|
+
# Use tuple format: (msd_values, msd_times)
|
|
197
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3, 0.4])
|
|
198
|
+
msd_times = np.array([0, 1, 2, 3, 4])
|
|
199
|
+
msd_data = (msd_values, msd_times)
|
|
200
|
+
|
|
201
|
+
csv_file = os.path.join(temp_dir, 'msd_test.csv')
|
|
202
|
+
save_msd_data(msd_data, csv_file, output_dir=temp_dir)
|
|
203
|
+
|
|
204
|
+
assert os.path.exists(csv_file)
|
|
205
|
+
|
|
206
|
+
# Read back and verify
|
|
207
|
+
df = pd.read_csv(csv_file)
|
|
208
|
+
assert len(df) > 0
|
|
209
|
+
|
|
210
|
+
shutil.rmtree(temp_dir)
|
|
211
|
+
|
|
212
|
+
def test_save_msd_data_creates_directory(self):
|
|
213
|
+
"""Test that output directory is created."""
|
|
214
|
+
temp_dir = tempfile.mkdtemp()
|
|
215
|
+
output_subdir = os.path.join(temp_dir, 'new_output')
|
|
216
|
+
|
|
217
|
+
# Use tuple format: (msd_values, msd_times)
|
|
218
|
+
msd_values = np.array([0, 0.1, 0.2])
|
|
219
|
+
msd_times = np.array([0, 1, 2])
|
|
220
|
+
msd_data = (msd_values, msd_times)
|
|
221
|
+
|
|
222
|
+
csv_file = os.path.join(output_subdir, 'msd.csv')
|
|
223
|
+
save_msd_data(msd_data, csv_file, output_dir=output_subdir)
|
|
224
|
+
|
|
225
|
+
assert os.path.exists(output_subdir)
|
|
226
|
+
|
|
227
|
+
shutil.rmtree(temp_dir)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TestPlotDiffusionTimeSeries:
|
|
231
|
+
"""Test diffusion time series plotting."""
|
|
232
|
+
|
|
233
|
+
def test_plot_diffusion_time_series_basic(self):
|
|
234
|
+
"""Test basic plotting functionality."""
|
|
235
|
+
temp_dir = tempfile.mkdtemp()
|
|
236
|
+
|
|
237
|
+
msd_times = np.array([0, 1, 2, 3, 4, 5])
|
|
238
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5])
|
|
239
|
+
|
|
240
|
+
# Plot without saving (just test it doesn't crash)
|
|
241
|
+
plot_diffusion_time_series(msd_times, msd_values, min_window=2)
|
|
242
|
+
|
|
243
|
+
shutil.rmtree(temp_dir)
|
|
244
|
+
|
|
245
|
+
def test_plot_diffusion_with_intercept(self):
|
|
246
|
+
"""Test plotting with intercept."""
|
|
247
|
+
msd_times = np.array([0, 1, 2, 3, 4, 5])
|
|
248
|
+
msd_values = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6])
|
|
249
|
+
|
|
250
|
+
# Should not raise error
|
|
251
|
+
plot_diffusion_time_series(msd_times, msd_values, with_intercept=True, min_window=2)
|
|
252
|
+
|
|
253
|
+
def test_plot_diffusion_dimension_variations(self):
|
|
254
|
+
"""Test plotting with different dimensions."""
|
|
255
|
+
msd_times = np.array([0, 1, 2, 3, 4])
|
|
256
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3, 0.4])
|
|
257
|
+
|
|
258
|
+
for dim in [1, 2, 3]:
|
|
259
|
+
plot_diffusion_time_series(msd_times, msd_values, dimension=dim, min_window=2)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class TestBlockAveragingError:
|
|
263
|
+
"""Test block averaging error analysis."""
|
|
264
|
+
|
|
265
|
+
def test_block_averaging_error_basic(self):
|
|
266
|
+
"""Test basic block averaging."""
|
|
267
|
+
msd_times = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
|
268
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
|
|
269
|
+
|
|
270
|
+
result = block_averaging_error(msd_times, msd_values, n_blocks=5)
|
|
271
|
+
|
|
272
|
+
assert result is not None
|
|
273
|
+
# Should return tuple or similar structure
|
|
274
|
+
assert isinstance(result, (tuple, dict, type(None)))
|
|
275
|
+
|
|
276
|
+
def test_block_averaging_different_block_counts(self):
|
|
277
|
+
"""Test with different block counts."""
|
|
278
|
+
msd_times = np.arange(0, 20, 1.0)
|
|
279
|
+
msd_values = msd_times * 0.1
|
|
280
|
+
|
|
281
|
+
for n_blocks in [2, 3, 5, 10]:
|
|
282
|
+
result = block_averaging_error(msd_times, msd_values, n_blocks=n_blocks)
|
|
283
|
+
assert result is not None
|
|
284
|
+
|
|
285
|
+
def test_block_averaging_dimension_variations(self):
|
|
286
|
+
"""Test block averaging with different dimensions."""
|
|
287
|
+
msd_times = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
|
288
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
|
|
289
|
+
|
|
290
|
+
for dim in [1, 2, 3]:
|
|
291
|
+
result = block_averaging_error(msd_times, msd_values, dimension=dim, n_blocks=2)
|
|
292
|
+
assert result is not None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class TestCalculateFrameMSDWindowed:
|
|
296
|
+
"""Test windowed frame MSD calculation."""
|
|
297
|
+
|
|
298
|
+
def test_calculate_frame_msd_windowed_basic(self):
|
|
299
|
+
"""Test basic windowed MSD calculation."""
|
|
300
|
+
pos_i = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
301
|
+
pos_j = np.array([[0.1, 0, 0], [1.1, 0, 0], [0.1, 1, 0]])
|
|
302
|
+
atom_indices = np.array([0, 1, 2])
|
|
303
|
+
|
|
304
|
+
result = calculate_frame_msd_windowed(pos_i, pos_j, atom_indices, msd_direction=False)
|
|
305
|
+
|
|
306
|
+
assert isinstance(result, (float, np.floating))
|
|
307
|
+
assert result > 0
|
|
308
|
+
|
|
309
|
+
def test_calculate_frame_msd_windowed_directional(self):
|
|
310
|
+
"""Test windowed directional MSD."""
|
|
311
|
+
pos_i = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
312
|
+
pos_j = np.array([[0.1, 0.2, 0.3], [1.1, 0, 0], [0.1, 1, 0]])
|
|
313
|
+
atom_indices = np.array([0, 1, 2])
|
|
314
|
+
|
|
315
|
+
result = calculate_frame_msd_windowed(pos_i, pos_j, atom_indices, msd_direction=True)
|
|
316
|
+
|
|
317
|
+
assert isinstance(result, tuple)
|
|
318
|
+
assert len(result) == 3 # msd_x, msd_y, msd_z
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class TestCalculateMSDWindowed:
|
|
322
|
+
"""Test windowed MSD calculation for trajectories."""
|
|
323
|
+
|
|
324
|
+
@pytest.fixture
|
|
325
|
+
def window_trajectory(self):
|
|
326
|
+
"""Create trajectory for windowed tests."""
|
|
327
|
+
frames = []
|
|
328
|
+
for i in range(15):
|
|
329
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
330
|
+
atoms.positions += np.array([0.05*i, 0.05*i, 0])
|
|
331
|
+
frames.append(atoms)
|
|
332
|
+
return frames
|
|
333
|
+
|
|
334
|
+
def test_calculate_msd_windowed_basic(self, window_trajectory):
|
|
335
|
+
"""Test basic windowed MSD calculation."""
|
|
336
|
+
timestep = 1.0
|
|
337
|
+
atom_indices = np.array([0, 1, 2])
|
|
338
|
+
|
|
339
|
+
# Test without window_size parameter (not supported)
|
|
340
|
+
result = calculate_msd_windowed(
|
|
341
|
+
window_trajectory,
|
|
342
|
+
timestep,
|
|
343
|
+
atom_indices=atom_indices
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
assert result is not None
|
|
347
|
+
assert len(result) == 2
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class TestMSDAnalysis:
|
|
351
|
+
"""Test complete MSD analysis workflow."""
|
|
352
|
+
|
|
353
|
+
@pytest.fixture
|
|
354
|
+
def test_trajectory_file(self):
|
|
355
|
+
"""Create test trajectory file."""
|
|
356
|
+
temp_dir = tempfile.mkdtemp()
|
|
357
|
+
traj_path = os.path.join(temp_dir, 'test_analysis.traj')
|
|
358
|
+
|
|
359
|
+
frames = []
|
|
360
|
+
for i in range(10):
|
|
361
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
362
|
+
atoms.positions += np.random.rand(3, 3) * 0.05 * i
|
|
363
|
+
atoms.cell = [10, 10, 10]
|
|
364
|
+
atoms.pbc = True
|
|
365
|
+
frames.append(atoms)
|
|
366
|
+
|
|
367
|
+
write(traj_path, frames)
|
|
368
|
+
yield traj_path
|
|
369
|
+
shutil.rmtree(temp_dir)
|
|
370
|
+
|
|
371
|
+
def test_msd_analysis_basic(self, test_trajectory_file):
|
|
372
|
+
"""Test basic MSD analysis."""
|
|
373
|
+
result = msd_analysis(
|
|
374
|
+
test_trajectory_file,
|
|
375
|
+
timestep_fs=1.0,
|
|
376
|
+
indices_path=None # Use correct parameter name
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Should complete without error
|
|
380
|
+
assert result is not None
|
|
381
|
+
|
|
382
|
+
def test_msd_analysis_with_indices_path(self, test_trajectory_file):
|
|
383
|
+
"""Test analysis with indices_path parameter."""
|
|
384
|
+
# Create temp indices file
|
|
385
|
+
temp_dir = tempfile.mkdtemp()
|
|
386
|
+
indices_file = os.path.join(temp_dir, 'indices.npy')
|
|
387
|
+
np.save(indices_file, np.array([0, 1]))
|
|
388
|
+
|
|
389
|
+
result = msd_analysis(
|
|
390
|
+
test_trajectory_file,
|
|
391
|
+
timestep_fs=1.0,
|
|
392
|
+
indices_path=indices_file
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
assert result is not None
|
|
396
|
+
shutil.rmtree(temp_dir)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class TestMSDParameterVariations:
|
|
400
|
+
"""Test MSD calculations with various parameter combinations."""
|
|
401
|
+
|
|
402
|
+
@pytest.fixture
|
|
403
|
+
def varied_trajectory(self):
|
|
404
|
+
"""Create trajectory with varied properties."""
|
|
405
|
+
frames = []
|
|
406
|
+
for i in range(8):
|
|
407
|
+
atoms = Atoms('H4O2', positions=[
|
|
408
|
+
[0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0],
|
|
409
|
+
[0.5, 1, 0], [1.5, 1, 0]
|
|
410
|
+
])
|
|
411
|
+
atoms.positions += np.random.rand(6, 3) * 0.02 * i
|
|
412
|
+
atoms.cell = [10, 10, 10]
|
|
413
|
+
frames.append(atoms)
|
|
414
|
+
return frames
|
|
415
|
+
|
|
416
|
+
def test_msd_with_atom_subset_variations(self, varied_trajectory):
|
|
417
|
+
"""Test MSD with different atom subsets."""
|
|
418
|
+
timestep = 1.0
|
|
419
|
+
|
|
420
|
+
subsets = [
|
|
421
|
+
np.array([0]),
|
|
422
|
+
np.array([0, 1]),
|
|
423
|
+
np.array([0, 1, 2, 3]),
|
|
424
|
+
np.array([0, 1, 2, 3, 4, 5])
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
for atom_indices in subsets:
|
|
428
|
+
result = calculate_msd(varied_trajectory, timestep, atom_indices=atom_indices)
|
|
429
|
+
assert result is not None
|
|
430
|
+
msd_vals, msd_times = result
|
|
431
|
+
assert len(msd_vals) > 0
|
|
432
|
+
|
|
433
|
+
def test_msd_with_different_timesteps(self, varied_trajectory):
|
|
434
|
+
"""Test MSD with different timestep values."""
|
|
435
|
+
atom_indices = np.array([0, 1])
|
|
436
|
+
|
|
437
|
+
for timestep in [0.5, 1.0, 2.0, 5.0]:
|
|
438
|
+
result = calculate_msd(varied_trajectory, timestep, atom_indices=atom_indices)
|
|
439
|
+
msd_vals, msd_times = result
|
|
440
|
+
|
|
441
|
+
# Larger timestep should result in larger time values
|
|
442
|
+
assert msd_times[-1] > 0
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class TestMSDIntegration:
|
|
446
|
+
"""Integration tests combining multiple MSD functions."""
|
|
447
|
+
|
|
448
|
+
def test_full_msd_workflow(self):
|
|
449
|
+
"""Test complete workflow from trajectory to diffusion coefficient."""
|
|
450
|
+
# Create trajectory
|
|
451
|
+
frames = []
|
|
452
|
+
for i in range(10):
|
|
453
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
454
|
+
atoms.positions += np.array([0.02*i, 0.02*i, 0])
|
|
455
|
+
frames.append(atoms)
|
|
456
|
+
|
|
457
|
+
# Calculate MSD
|
|
458
|
+
timestep = 1.0
|
|
459
|
+
atom_indices = np.array([0, 1, 2])
|
|
460
|
+
msd_values, msd_times = calculate_msd(frames, timestep, atom_indices=atom_indices)
|
|
461
|
+
|
|
462
|
+
# Just verify the MSD calculation worked
|
|
463
|
+
assert len(msd_values) > 0
|
|
464
|
+
assert len(msd_times) > 0
|
|
465
|
+
assert msd_values[0] >= 0
|
|
466
|
+
|
|
467
|
+
def test_msd_with_different_atom_types(self):
|
|
468
|
+
"""Test MSD calculation distinguishing atom types."""
|
|
469
|
+
frames = []
|
|
470
|
+
for i in range(8):
|
|
471
|
+
atoms = Atoms('H2O2', positions=[
|
|
472
|
+
[0, 0, 0], [1, 0, 0], # H atoms
|
|
473
|
+
[0.5, 0.5, 0], [1.5, 0.5, 0] # O atoms
|
|
474
|
+
])
|
|
475
|
+
atoms.positions += np.random.rand(4, 3) * 0.01 * i
|
|
476
|
+
frames.append(atoms)
|
|
477
|
+
|
|
478
|
+
timestep = 1.0
|
|
479
|
+
|
|
480
|
+
# MSD for H atoms only
|
|
481
|
+
h_indices = np.array([0, 1])
|
|
482
|
+
msd_h, time_h = calculate_msd(frames, timestep, atom_indices=h_indices)
|
|
483
|
+
|
|
484
|
+
# MSD for O atoms only
|
|
485
|
+
o_indices = np.array([2, 3])
|
|
486
|
+
msd_o, time_o = calculate_msd(frames, timestep, atom_indices=o_indices)
|
|
487
|
+
|
|
488
|
+
assert len(msd_h) > 0
|
|
489
|
+
assert len(msd_o) > 0
|
|
490
|
+
# Both should have same time array
|
|
491
|
+
assert len(time_h) == len(time_o)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class TestMSDEdgeCases:
|
|
495
|
+
"""Test MSD with edge cases."""
|
|
496
|
+
|
|
497
|
+
def test_msd_two_frame_trajectory(self):
|
|
498
|
+
"""Test MSD with minimal 2-frame trajectory."""
|
|
499
|
+
frames = [
|
|
500
|
+
Atoms('H2', positions=[[0, 0, 0], [1, 0, 0]]),
|
|
501
|
+
Atoms('H2', positions=[[0.1, 0, 0], [1.1, 0, 0]])
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
result = calculate_msd(frames, 1.0, atom_indices=np.array([0, 1]))
|
|
505
|
+
assert result is not None
|
|
506
|
+
|
|
507
|
+
def test_msd_large_trajectory(self):
|
|
508
|
+
"""Test MSD with larger trajectory."""
|
|
509
|
+
frames = []
|
|
510
|
+
for i in range(50):
|
|
511
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
512
|
+
atoms.positions += np.random.rand(3, 3) * 0.01 * i
|
|
513
|
+
frames.append(atoms)
|
|
514
|
+
|
|
515
|
+
result = calculate_msd(frames, 1.0, atom_indices=np.array([0, 1, 2]))
|
|
516
|
+
msd_vals, msd_times = result
|
|
517
|
+
|
|
518
|
+
# MSD calculation may return N or N+1 points
|
|
519
|
+
assert len(msd_times) >= 50
|
|
520
|
+
assert len(msd_times) <= 51
|
|
521
|
+
# MSD values should increase over time (random walk)
|
|
522
|
+
assert msd_vals[0] == 0 # Initial MSD is zero
|