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,151 @@
|
|
|
1
|
+
"""Extended tests for error analysis module."""
|
|
2
|
+
import pytest
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from CRISP.simulation_utility.error_analysis import (
|
|
6
|
+
optimal_lag,
|
|
7
|
+
vector_acf,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestErrorAnalysisBasic:
|
|
12
|
+
"""Basic error analysis tests."""
|
|
13
|
+
|
|
14
|
+
def test_optimal_lag_converged(self):
|
|
15
|
+
"""Test optimal lag finding for converged ACF."""
|
|
16
|
+
acf_values = np.array([1.0, 0.9, 0.7, 0.4, 0.1, 0.02, 0.0, -0.01])
|
|
17
|
+
|
|
18
|
+
lag = optimal_lag(acf_values, threshold=0.05)
|
|
19
|
+
|
|
20
|
+
# Should find lag where ACF drops below threshold
|
|
21
|
+
assert lag >= 0
|
|
22
|
+
assert lag < len(acf_values)
|
|
23
|
+
|
|
24
|
+
def test_optimal_lag_not_converged(self):
|
|
25
|
+
"""Test optimal lag when ACF doesn't converge (raises warning)."""
|
|
26
|
+
acf_values = np.array([1.0, 0.95, 0.90, 0.85, 0.80])
|
|
27
|
+
|
|
28
|
+
with pytest.warns(UserWarning):
|
|
29
|
+
lag = optimal_lag(acf_values, threshold=0.05)
|
|
30
|
+
|
|
31
|
+
# Should return last index when not converged
|
|
32
|
+
assert lag == len(acf_values) - 1
|
|
33
|
+
|
|
34
|
+
def test_optimal_lag_immediate_convergence(self):
|
|
35
|
+
"""Test optimal lag with immediate convergence."""
|
|
36
|
+
acf_values = np.array([1.0, 0.01, 0.0])
|
|
37
|
+
|
|
38
|
+
lag = optimal_lag(acf_values, threshold=0.05)
|
|
39
|
+
|
|
40
|
+
assert lag == 1
|
|
41
|
+
|
|
42
|
+
def test_vector_acf_basic(self):
|
|
43
|
+
"""Test vector ACF calculation."""
|
|
44
|
+
data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
|
|
45
|
+
|
|
46
|
+
acf_result = vector_acf(data, max_lag=2)
|
|
47
|
+
|
|
48
|
+
assert acf_result is not None
|
|
49
|
+
assert isinstance(acf_result, (np.ndarray, list, tuple))
|
|
50
|
+
|
|
51
|
+
def test_vector_acf_1d_input(self):
|
|
52
|
+
"""Test vector ACF with properly shaped 2D input."""
|
|
53
|
+
# vector_acf expects 2D input with shape (n_frames, n_dimensions)
|
|
54
|
+
# Create 2D array instead of 1D
|
|
55
|
+
data = np.array([[1, 0], [2, 1], [3, 2], [4, 3], [5, 4], [6, 5], [7, 6], [8, 7]])
|
|
56
|
+
|
|
57
|
+
acf_result = vector_acf(data, max_lag=3)
|
|
58
|
+
|
|
59
|
+
assert acf_result is not None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestErrorAnalysisParametrized:
|
|
63
|
+
"""Parametrized error analysis tests."""
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("threshold", [0.01, 0.05, 0.1, 0.2])
|
|
66
|
+
def test_optimal_lag_various_thresholds(self, threshold):
|
|
67
|
+
"""Test optimal lag with different thresholds."""
|
|
68
|
+
acf_values = np.array([1.0, 0.8, 0.5, 0.2, 0.05, 0.01, 0.001])
|
|
69
|
+
|
|
70
|
+
lag = optimal_lag(acf_values, threshold=threshold)
|
|
71
|
+
|
|
72
|
+
assert lag >= 0
|
|
73
|
+
assert lag < len(acf_values)
|
|
74
|
+
|
|
75
|
+
@pytest.mark.parametrize("max_lag", [1, 2, 5, 10])
|
|
76
|
+
def test_vector_acf_various_lags(self, max_lag):
|
|
77
|
+
"""Test vector ACF with different max lags."""
|
|
78
|
+
data = np.random.random((20, 3))
|
|
79
|
+
|
|
80
|
+
acf_result = vector_acf(data, max_lag=max_lag)
|
|
81
|
+
|
|
82
|
+
assert acf_result is not None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestErrorAnalysisEdgeCases:
|
|
86
|
+
"""Test error analysis edge cases."""
|
|
87
|
+
|
|
88
|
+
def test_optimal_lag_empty_array(self):
|
|
89
|
+
"""Test optimal lag with very short array."""
|
|
90
|
+
acf_values = np.array([1.0])
|
|
91
|
+
|
|
92
|
+
lag = optimal_lag(acf_values, threshold=0.05)
|
|
93
|
+
|
|
94
|
+
assert lag == 0
|
|
95
|
+
|
|
96
|
+
def test_optimal_lag_negative_values(self):
|
|
97
|
+
"""Test optimal lag with negative ACF values."""
|
|
98
|
+
acf_values = np.array([1.0, 0.5, 0.0, -0.3, -0.5])
|
|
99
|
+
|
|
100
|
+
lag = optimal_lag(acf_values, threshold=0.05)
|
|
101
|
+
|
|
102
|
+
assert lag >= 0
|
|
103
|
+
assert lag < len(acf_values)
|
|
104
|
+
|
|
105
|
+
def test_vector_acf_short_data(self):
|
|
106
|
+
"""Test vector ACF with short data."""
|
|
107
|
+
data = np.array([[1, 2], [3, 4], [5, 6]])
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
acf_result = vector_acf(data, max_lag=1)
|
|
111
|
+
assert acf_result is not None
|
|
112
|
+
except (ValueError, IndexError):
|
|
113
|
+
pass # Acceptable for short data
|
|
114
|
+
|
|
115
|
+
def test_vector_acf_single_sample(self):
|
|
116
|
+
"""Test vector ACF with single sample."""
|
|
117
|
+
data = np.array([[1, 2, 3]])
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
acf_result = vector_acf(data, max_lag=1)
|
|
121
|
+
# May raise error or return zeros
|
|
122
|
+
assert acf_result is not None or acf_result is None
|
|
123
|
+
except (ValueError, ZeroDivisionError):
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestErrorAnalysisIntegration:
|
|
128
|
+
"""Integration tests for error analysis."""
|
|
129
|
+
|
|
130
|
+
def test_lag_finding_workflow(self):
|
|
131
|
+
"""Test complete workflow of finding optimal lag."""
|
|
132
|
+
# Create synthetic data with known correlation
|
|
133
|
+
np.random.seed(42)
|
|
134
|
+
data = np.cumsum(np.random.randn(100, 3), axis=0)
|
|
135
|
+
|
|
136
|
+
acf_result = vector_acf(data, max_lag=10)
|
|
137
|
+
assert acf_result is not None
|
|
138
|
+
|
|
139
|
+
# Convert to array if needed
|
|
140
|
+
if isinstance(acf_result, (list, tuple)):
|
|
141
|
+
acf_array = np.array(acf_result)
|
|
142
|
+
else:
|
|
143
|
+
acf_array = acf_result
|
|
144
|
+
|
|
145
|
+
if len(acf_array) > 0:
|
|
146
|
+
lag = optimal_lag(acf_array, threshold=0.05)
|
|
147
|
+
assert lag >= 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == '__main__':
|
|
151
|
+
pytest.main([__file__, '-v'])
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Tests for interatomic distances calculation module."""
|
|
2
|
+
import pytest
|
|
3
|
+
import numpy as np
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import shutil
|
|
7
|
+
from CRISP.simulation_utility.interatomic_distances import (
|
|
8
|
+
indices,
|
|
9
|
+
distance_calculation,
|
|
10
|
+
save_distance_matrices,
|
|
11
|
+
calculate_interatomic_distances,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from ase import Atoms
|
|
16
|
+
ASE_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
ASE_AVAILABLE = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def temp_output_dir():
|
|
23
|
+
"""Create temporary directory for outputs."""
|
|
24
|
+
temp_dir = tempfile.mkdtemp()
|
|
25
|
+
yield temp_dir
|
|
26
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def sample_atoms():
|
|
31
|
+
"""Create sample atoms object."""
|
|
32
|
+
if not ASE_AVAILABLE:
|
|
33
|
+
return None
|
|
34
|
+
positions = np.array([
|
|
35
|
+
[0.0, 0.0, 0.0],
|
|
36
|
+
[1.0, 0.0, 0.0],
|
|
37
|
+
[0.5, 0.866, 0.0],
|
|
38
|
+
[0.5, 0.289, 0.816]
|
|
39
|
+
])
|
|
40
|
+
return Atoms('H4', positions=positions, cell=[10, 10, 10], pbc=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.skipif(not ASE_AVAILABLE, reason="ASE not available")
|
|
44
|
+
class TestIndicesFunction:
|
|
45
|
+
"""Test the indices extraction function."""
|
|
46
|
+
|
|
47
|
+
def test_indices_all_atoms(self, sample_atoms):
|
|
48
|
+
"""Test extracting all atom indices."""
|
|
49
|
+
result = indices(sample_atoms, "all")
|
|
50
|
+
assert isinstance(result, np.ndarray)
|
|
51
|
+
assert len(result) == 4
|
|
52
|
+
np.testing.assert_array_equal(result, np.array([0, 1, 2, 3]))
|
|
53
|
+
|
|
54
|
+
def test_indices_none_defaults_to_all(self, sample_atoms):
|
|
55
|
+
"""Test that None defaults to all atoms."""
|
|
56
|
+
result = indices(sample_atoms, None)
|
|
57
|
+
assert isinstance(result, np.ndarray)
|
|
58
|
+
assert len(result) == 4
|
|
59
|
+
|
|
60
|
+
def test_indices_with_list_integers(self, sample_atoms):
|
|
61
|
+
"""Test extracting specific atom indices by integers."""
|
|
62
|
+
result = indices(sample_atoms, [0, 2])
|
|
63
|
+
assert isinstance(result, np.ndarray)
|
|
64
|
+
np.testing.assert_array_equal(result, np.array([0, 2]))
|
|
65
|
+
|
|
66
|
+
def test_indices_with_chemical_symbols(self, sample_atoms):
|
|
67
|
+
"""Test extracting atoms by chemical symbol."""
|
|
68
|
+
result = indices(sample_atoms, ["H"])
|
|
69
|
+
assert isinstance(result, np.ndarray)
|
|
70
|
+
assert len(result) == 4 # All atoms are H
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.skipif(not ASE_AVAILABLE, reason="ASE not available")
|
|
74
|
+
class TestDistanceCalculation:
|
|
75
|
+
"""Test distance calculation function."""
|
|
76
|
+
|
|
77
|
+
def test_distance_calculation_invalid_file(self):
|
|
78
|
+
"""Test with nonexistent trajectory file."""
|
|
79
|
+
with pytest.raises((FileNotFoundError, ValueError)):
|
|
80
|
+
distance_calculation(
|
|
81
|
+
traj_path="nonexistent_trajectory.traj",
|
|
82
|
+
frame_skip=1,
|
|
83
|
+
index_type="all"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_distance_calculation_module_import(self):
|
|
87
|
+
"""Test that module can be imported."""
|
|
88
|
+
from CRISP.simulation_utility import interatomic_distances
|
|
89
|
+
assert hasattr(interatomic_distances, 'distance_calculation')
|
|
90
|
+
assert hasattr(interatomic_distances, 'calculate_interatomic_distances')
|
|
91
|
+
assert hasattr(interatomic_distances, 'indices')
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.skipif(not ASE_AVAILABLE, reason="ASE not available")
|
|
95
|
+
class TestSaveDistanceMatrices:
|
|
96
|
+
"""Test saving distance matrices."""
|
|
97
|
+
|
|
98
|
+
def test_save_distance_matrices_basic(self, temp_output_dir, sample_atoms):
|
|
99
|
+
"""Test saving distance matrices."""
|
|
100
|
+
# Create sample distance matrices
|
|
101
|
+
dm1 = sample_atoms.get_all_distances(mic=True)
|
|
102
|
+
dm2 = sample_atoms.get_all_distances(mic=True)
|
|
103
|
+
|
|
104
|
+
full_dms = [dm1, dm2]
|
|
105
|
+
sub_dms = [dm1[:2, :2], dm2[:2, :2]]
|
|
106
|
+
|
|
107
|
+
save_distance_matrices(
|
|
108
|
+
full_dms=full_dms,
|
|
109
|
+
sub_dms=sub_dms,
|
|
110
|
+
index_type="all",
|
|
111
|
+
output_dir=temp_output_dir
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Check that output file was created
|
|
115
|
+
output_file = os.path.join(temp_output_dir, "distance_matrices.pkl")
|
|
116
|
+
assert os.path.exists(output_file)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.skipif(not ASE_AVAILABLE, reason="ASE not available")
|
|
120
|
+
class TestCalculateInteratomicDistances:
|
|
121
|
+
"""Test main wrapper function."""
|
|
122
|
+
|
|
123
|
+
def test_calculate_interatomic_distances_basic(self, sample_atoms, temp_output_dir):
|
|
124
|
+
"""Test basic interatomic distance calculation."""
|
|
125
|
+
# Create a temporary trajectory file
|
|
126
|
+
traj_file = os.path.join(temp_output_dir, "test.traj")
|
|
127
|
+
sample_atoms.write(traj_file)
|
|
128
|
+
|
|
129
|
+
result = calculate_interatomic_distances(
|
|
130
|
+
traj_path=traj_file,
|
|
131
|
+
frame_skip=1,
|
|
132
|
+
index_type="all",
|
|
133
|
+
output_dir=temp_output_dir,
|
|
134
|
+
save_results=True
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert isinstance(result, dict)
|
|
138
|
+
assert "full_dms" in result
|
|
139
|
+
assert isinstance(result["full_dms"], list)
|
|
140
|
+
assert len(result["full_dms"]) > 0
|
|
141
|
+
|
|
142
|
+
def test_calculate_with_atom_indices(self, sample_atoms, temp_output_dir):
|
|
143
|
+
"""Test calculation with specific atom indices."""
|
|
144
|
+
traj_file = os.path.join(temp_output_dir, "test.traj")
|
|
145
|
+
sample_atoms.write(traj_file)
|
|
146
|
+
|
|
147
|
+
result = calculate_interatomic_distances(
|
|
148
|
+
traj_path=traj_file,
|
|
149
|
+
frame_skip=1,
|
|
150
|
+
index_type=[0, 1],
|
|
151
|
+
output_dir=temp_output_dir,
|
|
152
|
+
save_results=True
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
assert "full_dms" in result
|
|
156
|
+
assert "sub_dms" in result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@pytest.mark.skipif(not ASE_AVAILABLE, reason="ASE not available")
|
|
160
|
+
class TestInteratomicDistancesEdgeCases:
|
|
161
|
+
"""Test edge cases and error handling."""
|
|
162
|
+
|
|
163
|
+
def test_single_atom(self, temp_output_dir):
|
|
164
|
+
"""Test with single atom."""
|
|
165
|
+
if not ASE_AVAILABLE:
|
|
166
|
+
pytest.skip("ASE not available")
|
|
167
|
+
|
|
168
|
+
atoms = Atoms('H', positions=[[0, 0, 0]], cell=[10, 10, 10], pbc=True)
|
|
169
|
+
traj_file = os.path.join(temp_output_dir, "single_atom.traj")
|
|
170
|
+
atoms.write(traj_file)
|
|
171
|
+
|
|
172
|
+
result = calculate_interatomic_distances(
|
|
173
|
+
traj_path=traj_file,
|
|
174
|
+
frame_skip=1,
|
|
175
|
+
output_dir=temp_output_dir,
|
|
176
|
+
save_results=False
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Single atom should have 1x1 distance matrix
|
|
180
|
+
assert result["full_dms"][0].shape == (1, 1)
|
|
181
|
+
|
|
182
|
+
def test_invalid_trajectory_file(self, temp_output_dir):
|
|
183
|
+
"""Test with invalid trajectory file."""
|
|
184
|
+
with pytest.raises((FileNotFoundError, ValueError)):
|
|
185
|
+
calculate_interatomic_distances(
|
|
186
|
+
traj_path="nonexistent.traj",
|
|
187
|
+
frame_skip=1,
|
|
188
|
+
output_dir=temp_output_dir
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def test_distance_matrix_symmetry(self, sample_atoms, temp_output_dir):
|
|
192
|
+
"""Test that distance matrices are symmetric."""
|
|
193
|
+
traj_file = os.path.join(temp_output_dir, "test.traj")
|
|
194
|
+
sample_atoms.write(traj_file)
|
|
195
|
+
|
|
196
|
+
result = calculate_interatomic_distances(
|
|
197
|
+
traj_path=traj_file,
|
|
198
|
+
frame_skip=1,
|
|
199
|
+
output_dir=temp_output_dir,
|
|
200
|
+
save_results=False
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
dm = result["full_dms"][0]
|
|
204
|
+
|
|
205
|
+
# Check symmetry
|
|
206
|
+
np.testing.assert_array_almost_equal(dm, dm.T, decimal=5)
|
|
207
|
+
|
|
208
|
+
def test_distance_matrix_diagonal_zeros(self, sample_atoms, temp_output_dir):
|
|
209
|
+
"""Test that distance matrix diagonal is zero."""
|
|
210
|
+
traj_file = os.path.join(temp_output_dir, "test.traj")
|
|
211
|
+
sample_atoms.write(traj_file)
|
|
212
|
+
|
|
213
|
+
result = calculate_interatomic_distances(
|
|
214
|
+
traj_path=traj_file,
|
|
215
|
+
frame_skip=1,
|
|
216
|
+
output_dir=temp_output_dir,
|
|
217
|
+
save_results=False
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
dm = result["full_dms"][0]
|
|
221
|
+
|
|
222
|
+
# Check diagonal is zero
|
|
223
|
+
np.testing.assert_array_almost_equal(np.diag(dm), np.zeros(len(dm)), decimal=5)
|