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,305 @@
|
|
|
1
|
+
"""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 CRISP.data_analysis.msd import (
|
|
9
|
+
calculate_diffusion_coefficient,
|
|
10
|
+
calculate_save_msd,
|
|
11
|
+
analyze_from_csv,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestMSDCalculations:
|
|
16
|
+
"""Test MSD calculation core functionality."""
|
|
17
|
+
|
|
18
|
+
def test_calculate_diffusion_coefficient_basic(self):
|
|
19
|
+
"""Test basic diffusion coefficient calculation."""
|
|
20
|
+
# Create linear MSD data (slope = 0.4)
|
|
21
|
+
msd_times = np.array([0, 1, 2, 3, 4, 5])
|
|
22
|
+
msd_values = np.array([0, 0.4, 0.8, 1.2, 1.6, 2.0])
|
|
23
|
+
|
|
24
|
+
D, error = calculate_diffusion_coefficient(
|
|
25
|
+
msd_times=msd_times,
|
|
26
|
+
msd_values=msd_values,
|
|
27
|
+
start_index=0,
|
|
28
|
+
end_index=len(msd_values),
|
|
29
|
+
with_intercept=True
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assert isinstance(D, float)
|
|
33
|
+
assert isinstance(error, float)
|
|
34
|
+
assert D > 0
|
|
35
|
+
assert error >= 0
|
|
36
|
+
|
|
37
|
+
def test_calculate_diffusion_without_intercept(self):
|
|
38
|
+
"""Test diffusion coefficient calculation without intercept."""
|
|
39
|
+
msd_times = np.array([0, 1, 2, 3, 4])
|
|
40
|
+
msd_values = np.array([0, 0.2, 0.4, 0.6, 0.8])
|
|
41
|
+
|
|
42
|
+
D, error = calculate_diffusion_coefficient(
|
|
43
|
+
msd_times=msd_times,
|
|
44
|
+
msd_values=msd_values,
|
|
45
|
+
start_index=0,
|
|
46
|
+
end_index=len(msd_values),
|
|
47
|
+
with_intercept=False
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
assert D > 0
|
|
51
|
+
assert error >= 0
|
|
52
|
+
|
|
53
|
+
def test_calculate_diffusion_subset(self):
|
|
54
|
+
"""Test diffusion coefficient with subset of data."""
|
|
55
|
+
msd_times = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
|
56
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 1.0, 2.0, 3.0])
|
|
57
|
+
|
|
58
|
+
# Use only first 5 points
|
|
59
|
+
D, error = calculate_diffusion_coefficient(
|
|
60
|
+
msd_times=msd_times,
|
|
61
|
+
msd_values=msd_values,
|
|
62
|
+
start_index=0,
|
|
63
|
+
end_index=5,
|
|
64
|
+
with_intercept=True
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert D > 0
|
|
68
|
+
assert error >= 0
|
|
69
|
+
|
|
70
|
+
def test_calculate_diffusion_different_start_end(self):
|
|
71
|
+
"""Test diffusion coefficient with different start/end indices."""
|
|
72
|
+
msd_times = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
|
73
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8])
|
|
74
|
+
|
|
75
|
+
# Use middle portion (indices 2-6)
|
|
76
|
+
D, error = calculate_diffusion_coefficient(
|
|
77
|
+
msd_times=msd_times,
|
|
78
|
+
msd_values=msd_values,
|
|
79
|
+
start_index=2,
|
|
80
|
+
end_index=7,
|
|
81
|
+
with_intercept=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assert D > 0
|
|
85
|
+
|
|
86
|
+
def test_calculate_diffusion_single_point(self):
|
|
87
|
+
"""Test with insufficient data points - returns result but with nan R²."""
|
|
88
|
+
msd_times = np.array([0])
|
|
89
|
+
msd_values = np.array([0])
|
|
90
|
+
|
|
91
|
+
# Function doesn't raise error, just returns result with warnings
|
|
92
|
+
result = calculate_diffusion_coefficient(
|
|
93
|
+
msd_times=msd_times,
|
|
94
|
+
msd_values=msd_values,
|
|
95
|
+
start_index=0,
|
|
96
|
+
end_index=1
|
|
97
|
+
)
|
|
98
|
+
# Should return a tuple
|
|
99
|
+
assert result is not None
|
|
100
|
+
assert len(result) == 2
|
|
101
|
+
|
|
102
|
+
def test_calculate_diffusion_empty_data(self):
|
|
103
|
+
"""Test with empty data."""
|
|
104
|
+
with pytest.raises((ValueError, IndexError)):
|
|
105
|
+
calculate_diffusion_coefficient(
|
|
106
|
+
msd_times=np.array([]),
|
|
107
|
+
msd_values=np.array([]),
|
|
108
|
+
start_index=0,
|
|
109
|
+
end_index=0
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestMSDFileOperations:
|
|
114
|
+
"""Test MSD file reading and writing."""
|
|
115
|
+
|
|
116
|
+
@pytest.fixture
|
|
117
|
+
def sample_csv_file(self):
|
|
118
|
+
"""Create sample MSD CSV file."""
|
|
119
|
+
temp_dir = tempfile.mkdtemp()
|
|
120
|
+
csv_path = os.path.join(temp_dir, 'msd_sample.csv')
|
|
121
|
+
|
|
122
|
+
# Create realistic MSD data
|
|
123
|
+
data = {
|
|
124
|
+
'Time (fs)': np.arange(0, 100, 10),
|
|
125
|
+
'MSD': np.array([0, 0.5, 1.2, 2.1, 3.2, 4.5, 5.8, 7.2, 8.8, 10.5])
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
df = pd.DataFrame(data)
|
|
129
|
+
df.to_csv(csv_path, index=False)
|
|
130
|
+
|
|
131
|
+
yield csv_path
|
|
132
|
+
shutil.rmtree(temp_dir)
|
|
133
|
+
|
|
134
|
+
def test_analyze_from_csv_basic(self, sample_csv_file):
|
|
135
|
+
"""Test MSD analysis from CSV file."""
|
|
136
|
+
D, error = analyze_from_csv(
|
|
137
|
+
csv_file=sample_csv_file,
|
|
138
|
+
fit_start=0,
|
|
139
|
+
fit_end=5,
|
|
140
|
+
with_intercept=True
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
assert isinstance(D, float)
|
|
144
|
+
assert isinstance(error, float)
|
|
145
|
+
assert D > 0
|
|
146
|
+
|
|
147
|
+
def test_analyze_from_csv_different_fit_range(self, sample_csv_file):
|
|
148
|
+
"""Test MSD analysis with different fit ranges."""
|
|
149
|
+
# Test with different ranges
|
|
150
|
+
D1, _ = analyze_from_csv(
|
|
151
|
+
csv_file=sample_csv_file,
|
|
152
|
+
fit_start=0,
|
|
153
|
+
fit_end=3,
|
|
154
|
+
with_intercept=True
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
D2, _ = analyze_from_csv(
|
|
158
|
+
csv_file=sample_csv_file,
|
|
159
|
+
fit_start=3,
|
|
160
|
+
fit_end=8,
|
|
161
|
+
with_intercept=True
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
assert D1 > 0
|
|
165
|
+
assert D2 > 0
|
|
166
|
+
assert D1 != D2 # Different ranges should give different results
|
|
167
|
+
|
|
168
|
+
def test_analyze_from_csv_invalid_file(self):
|
|
169
|
+
"""Test analysis with nonexistent file returns None."""
|
|
170
|
+
D, error = analyze_from_csv(
|
|
171
|
+
csv_file='nonexistent_file.csv',
|
|
172
|
+
fit_start=0,
|
|
173
|
+
fit_end=5
|
|
174
|
+
)
|
|
175
|
+
# Function catches error and returns (None, None)
|
|
176
|
+
assert D is None
|
|
177
|
+
assert error is None
|
|
178
|
+
|
|
179
|
+
def test_analyze_from_csv_with_blocks(self, sample_csv_file):
|
|
180
|
+
"""Test MSD analysis with block averaging."""
|
|
181
|
+
D, error = analyze_from_csv(
|
|
182
|
+
csv_file=sample_csv_file,
|
|
183
|
+
fit_start=0,
|
|
184
|
+
fit_end=5,
|
|
185
|
+
with_intercept=True,
|
|
186
|
+
n_blocks=2
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
assert D > 0
|
|
190
|
+
assert error >= 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TestMSDEdgeCases:
|
|
194
|
+
"""Test edge cases and error handling."""
|
|
195
|
+
|
|
196
|
+
def test_negative_msd_values(self):
|
|
197
|
+
"""Test handling of negative MSD values (physically impossible)."""
|
|
198
|
+
msd_times = np.array([0, 1, 2])
|
|
199
|
+
msd_values = np.array([0, -0.1, -0.2]) # Negative values
|
|
200
|
+
|
|
201
|
+
# Function should handle gracefully
|
|
202
|
+
D, error = calculate_diffusion_coefficient(
|
|
203
|
+
msd_times=msd_times,
|
|
204
|
+
msd_values=msd_values,
|
|
205
|
+
start_index=0,
|
|
206
|
+
end_index=3
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
assert isinstance(D, float)
|
|
210
|
+
|
|
211
|
+
def test_constant_msd_values(self):
|
|
212
|
+
"""Test with constant MSD values (no motion)."""
|
|
213
|
+
msd_times = np.array([0, 1, 2, 3, 4])
|
|
214
|
+
msd_values = np.array([0, 0, 0, 0, 0]) # No motion
|
|
215
|
+
|
|
216
|
+
D, error = calculate_diffusion_coefficient(
|
|
217
|
+
msd_times=msd_times,
|
|
218
|
+
msd_values=msd_values,
|
|
219
|
+
start_index=0,
|
|
220
|
+
end_index=5
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# D should be close to 0
|
|
224
|
+
assert D >= 0
|
|
225
|
+
assert D < 0.1
|
|
226
|
+
|
|
227
|
+
def test_mismatched_array_lengths(self):
|
|
228
|
+
"""Test with mismatched array lengths - may work due to slicing."""
|
|
229
|
+
# Function uses slicing with start_index:end_index, so mismatch may work
|
|
230
|
+
try:
|
|
231
|
+
result = calculate_diffusion_coefficient(
|
|
232
|
+
msd_times=np.array([0, 1, 2]),
|
|
233
|
+
msd_values=np.array([0, 1]), # Different length
|
|
234
|
+
start_index=0,
|
|
235
|
+
end_index=2
|
|
236
|
+
)
|
|
237
|
+
# If it works, result should be a tuple
|
|
238
|
+
assert result is not None
|
|
239
|
+
except (ValueError, IndexError):
|
|
240
|
+
# Or it may raise an error, which is also acceptable
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
def test_invalid_index_range(self):
|
|
244
|
+
"""Test with invalid start/end indices."""
|
|
245
|
+
msd_times = np.array([0, 1, 2, 3])
|
|
246
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3])
|
|
247
|
+
|
|
248
|
+
with pytest.raises((ValueError, IndexError)):
|
|
249
|
+
calculate_diffusion_coefficient(
|
|
250
|
+
msd_times=msd_times,
|
|
251
|
+
msd_values=msd_values,
|
|
252
|
+
start_index=5, # Out of bounds
|
|
253
|
+
end_index=10
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def test_start_greater_than_end(self):
|
|
257
|
+
"""Test with start index greater than end index."""
|
|
258
|
+
msd_times = np.array([0, 1, 2, 3, 4])
|
|
259
|
+
msd_values = np.array([0, 0.1, 0.2, 0.3, 0.4])
|
|
260
|
+
|
|
261
|
+
with pytest.raises((ValueError, Exception)):
|
|
262
|
+
calculate_diffusion_coefficient(
|
|
263
|
+
msd_times=msd_times,
|
|
264
|
+
msd_values=msd_values,
|
|
265
|
+
start_index=4,
|
|
266
|
+
end_index=2 # Start > End
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TestMSDIntegration:
|
|
271
|
+
"""Test MSD module integration."""
|
|
272
|
+
|
|
273
|
+
def test_msd_module_imports(self):
|
|
274
|
+
"""Test that all MSD functions can be imported."""
|
|
275
|
+
from CRISP.data_analysis.msd import (
|
|
276
|
+
calculate_diffusion_coefficient,
|
|
277
|
+
calculate_save_msd,
|
|
278
|
+
analyze_from_csv,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
assert callable(calculate_diffusion_coefficient)
|
|
282
|
+
assert callable(calculate_save_msd)
|
|
283
|
+
assert callable(analyze_from_csv)
|
|
284
|
+
|
|
285
|
+
def test_diffusion_values_reasonable(self):
|
|
286
|
+
"""Test that diffusion values are physically reasonable."""
|
|
287
|
+
# Create MSD data typical for molecular dynamics
|
|
288
|
+
msd_times = np.linspace(0, 1000, 50) # Time in fs
|
|
289
|
+
# D ≈ 1e-5 cm²/s = 1e-9 m²/s, in Angstrom²/fs: 1e-9 * 1e20 * 1e-15 ≈ 0.001
|
|
290
|
+
msd_values = 4 * 0.001 * msd_times # MSD = 4*D*t
|
|
291
|
+
|
|
292
|
+
D, error = calculate_diffusion_coefficient(
|
|
293
|
+
msd_times=msd_times,
|
|
294
|
+
msd_values=msd_values,
|
|
295
|
+
start_index=0,
|
|
296
|
+
end_index=len(msd_values)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# D should be positive and reasonable
|
|
300
|
+
assert D > 0
|
|
301
|
+
assert D < 10 # Not unreasonably large
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == '__main__':
|
|
305
|
+
pytest.main([__file__, '-v'])
|