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,184 @@
|
|
|
1
|
+
"""Extended tests for contact coordination module."""
|
|
2
|
+
import pytest
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ase import Atoms
|
|
5
|
+
|
|
6
|
+
from CRISP.data_analysis.contact_coordination import (
|
|
7
|
+
indices,
|
|
8
|
+
coordination_frame,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCoordinationBasic:
|
|
13
|
+
"""Basic coordination number tests."""
|
|
14
|
+
|
|
15
|
+
def test_indices_all_atoms(self):
|
|
16
|
+
"""Test indices function with 'all' specifier."""
|
|
17
|
+
atoms = Atoms('H2O', positions=[
|
|
18
|
+
[0.0, 0.0, 0.0],
|
|
19
|
+
[0.96, 0.0, 0.0],
|
|
20
|
+
[0.24, 0.93, 0.0]
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
idx = indices(atoms, "all")
|
|
24
|
+
assert len(idx) == 3
|
|
25
|
+
assert np.array_equal(idx, [0, 1, 2])
|
|
26
|
+
|
|
27
|
+
def test_indices_none_defaults_to_all(self):
|
|
28
|
+
"""Test that None defaults to all atoms."""
|
|
29
|
+
atoms = Atoms('H2O', positions=[
|
|
30
|
+
[0.0, 0.0, 0.0],
|
|
31
|
+
[0.96, 0.0, 0.0],
|
|
32
|
+
[0.24, 0.93, 0.0]
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
idx = indices(atoms, None)
|
|
36
|
+
assert len(idx) == 3
|
|
37
|
+
|
|
38
|
+
def test_indices_by_list(self):
|
|
39
|
+
"""Test indices with explicit list."""
|
|
40
|
+
atoms = Atoms('H2O', positions=[
|
|
41
|
+
[0.0, 0.0, 0.0],
|
|
42
|
+
[0.96, 0.0, 0.0],
|
|
43
|
+
[0.24, 0.93, 0.0]
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
idx = indices(atoms, [0, 2])
|
|
47
|
+
assert len(idx) == 2
|
|
48
|
+
assert np.array_equal(idx, [0, 2])
|
|
49
|
+
|
|
50
|
+
def test_indices_by_symbol(self):
|
|
51
|
+
"""Test indices selection by chemical symbol."""
|
|
52
|
+
atoms = Atoms('H2O', positions=[
|
|
53
|
+
[0.0, 0.0, 0.0],
|
|
54
|
+
[0.96, 0.0, 0.0],
|
|
55
|
+
[0.24, 0.93, 0.0]
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
h_indices = indices(atoms, ['H'])
|
|
59
|
+
assert len(h_indices) == 2
|
|
60
|
+
|
|
61
|
+
def test_coordination_frame_basic(self):
|
|
62
|
+
"""Test basic coordination analysis on single frame."""
|
|
63
|
+
atoms = Atoms('H2O', positions=[
|
|
64
|
+
[0.0, 0.0, 0.0],
|
|
65
|
+
[0.96, 0.0, 0.0],
|
|
66
|
+
[0.24, 0.93, 0.0]
|
|
67
|
+
])
|
|
68
|
+
atoms.set_cell([10, 10, 10])
|
|
69
|
+
atoms.set_pbc([True, True, True])
|
|
70
|
+
|
|
71
|
+
central = [0] # Oxygen
|
|
72
|
+
target = [1, 2] # Hydrogens
|
|
73
|
+
|
|
74
|
+
result = coordination_frame(atoms, central, target)
|
|
75
|
+
assert result is not None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestCoordinationParametrized:
|
|
79
|
+
"""Test coordination with parameter variations."""
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize("central", [0, 1, 2])
|
|
82
|
+
def test_coordination_different_central_atoms(self, central):
|
|
83
|
+
"""Test coordination for different central atoms."""
|
|
84
|
+
atoms = Atoms('H2O', positions=[
|
|
85
|
+
[0.0, 0.0, 0.0],
|
|
86
|
+
[0.96, 0.0, 0.0],
|
|
87
|
+
[0.24, 0.93, 0.0]
|
|
88
|
+
])
|
|
89
|
+
atoms.set_cell([10, 10, 10])
|
|
90
|
+
atoms.set_pbc([True, True, True])
|
|
91
|
+
|
|
92
|
+
target_atoms = [i for i in range(len(atoms)) if i != central]
|
|
93
|
+
result = coordination_frame(atoms, [central], target_atoms)
|
|
94
|
+
|
|
95
|
+
assert result is not None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestCoordinationPBC:
|
|
99
|
+
"""Test coordination with periodic boundary conditions."""
|
|
100
|
+
|
|
101
|
+
def test_coordination_with_pbc(self):
|
|
102
|
+
"""Test coordination considering PBC."""
|
|
103
|
+
atoms = Atoms('H2', positions=[
|
|
104
|
+
[0.1, 0.0, 0.0],
|
|
105
|
+
[9.9, 0.0, 0.0]
|
|
106
|
+
])
|
|
107
|
+
atoms.set_cell([10, 10, 10])
|
|
108
|
+
atoms.set_pbc([True, True, True])
|
|
109
|
+
|
|
110
|
+
result = coordination_frame(atoms, [0], [1])
|
|
111
|
+
assert result is not None
|
|
112
|
+
|
|
113
|
+
def test_coordination_without_pbc(self):
|
|
114
|
+
"""Test coordination without periodic boundary conditions."""
|
|
115
|
+
atoms = Atoms('H2', positions=[
|
|
116
|
+
[0.0, 0.0, 0.0],
|
|
117
|
+
[9.9, 0.0, 0.0]
|
|
118
|
+
])
|
|
119
|
+
atoms.set_cell([10, 10, 10])
|
|
120
|
+
atoms.set_pbc([False, False, False])
|
|
121
|
+
|
|
122
|
+
result = coordination_frame(atoms, [0], [1], mic=False)
|
|
123
|
+
assert result is not None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestCoordinationEdgeCases:
|
|
127
|
+
"""Test coordination edge cases."""
|
|
128
|
+
|
|
129
|
+
def test_coordination_single_atom(self):
|
|
130
|
+
"""Test coordination for single atom with empty target atoms."""
|
|
131
|
+
atoms = Atoms('H', positions=[[0.0, 0.0, 0.0]])
|
|
132
|
+
atoms.set_cell([10, 10, 10])
|
|
133
|
+
atoms.set_pbc([True, True, True])
|
|
134
|
+
|
|
135
|
+
# Empty target_atoms should raise ValueError due to indices() not handling empty list
|
|
136
|
+
with pytest.raises(ValueError, match="Invalid index type"):
|
|
137
|
+
result = coordination_frame(atoms, [0], [])
|
|
138
|
+
def test_coordination_multiple_central(self):
|
|
139
|
+
"""Test with multiple central atoms."""
|
|
140
|
+
atoms = Atoms('H2O', positions=[
|
|
141
|
+
[0.0, 0.0, 0.0],
|
|
142
|
+
[0.96, 0.0, 0.0],
|
|
143
|
+
[0.24, 0.93, 0.0]
|
|
144
|
+
])
|
|
145
|
+
atoms.set_cell([10, 10, 10])
|
|
146
|
+
atoms.set_pbc([True, True, True])
|
|
147
|
+
|
|
148
|
+
result = coordination_frame(atoms, [0, 1], [2])
|
|
149
|
+
assert result is not None
|
|
150
|
+
|
|
151
|
+
def test_indices_with_file_notation(self):
|
|
152
|
+
"""Test indices with invalid file (should raise error)."""
|
|
153
|
+
atoms = Atoms('H2O', positions=[
|
|
154
|
+
[0.0, 0.0, 0.0],
|
|
155
|
+
[0.96, 0.0, 0.0],
|
|
156
|
+
[0.24, 0.93, 0.0]
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
# Should handle invalid file gracefully or raise error
|
|
160
|
+
try:
|
|
161
|
+
idx = indices(atoms, "nonexistent.npy")
|
|
162
|
+
except (FileNotFoundError, OSError):
|
|
163
|
+
pass # Expected
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestCoordinationIntegration:
|
|
167
|
+
"""Integration tests for coordination."""
|
|
168
|
+
|
|
169
|
+
def test_coordination_water_cluster(self):
|
|
170
|
+
"""Test coordination in water-like cluster."""
|
|
171
|
+
atoms = Atoms('OH2', positions=[
|
|
172
|
+
[0.0, 0.0, 0.0], # O central
|
|
173
|
+
[0.96, 0.0, 0.0], # H
|
|
174
|
+
[0.24, 0.93, 0.0] # H
|
|
175
|
+
])
|
|
176
|
+
atoms.set_cell([10, 10, 10])
|
|
177
|
+
atoms.set_pbc([True, True, True])
|
|
178
|
+
|
|
179
|
+
result = coordination_frame(atoms, [0], [1, 2])
|
|
180
|
+
assert result is not None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == '__main__':
|
|
184
|
+
pytest.main([__file__, '-v'])
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Extended comprehensive tests for Contact Coordination module."""
|
|
2
|
+
import pytest
|
|
3
|
+
import numpy as np
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import shutil
|
|
7
|
+
from ase import Atoms
|
|
8
|
+
from ase.io import write
|
|
9
|
+
from CRISP.data_analysis.contact_coordination import (
|
|
10
|
+
indices,
|
|
11
|
+
coordination_frame,
|
|
12
|
+
coordination,
|
|
13
|
+
contacts,
|
|
14
|
+
contacts_frame,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestCoordinationIndices:
|
|
19
|
+
"""Test the indices helper function."""
|
|
20
|
+
|
|
21
|
+
def test_indices_all_string(self):
|
|
22
|
+
"""Test 'all' returns all atoms."""
|
|
23
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
24
|
+
result = indices(atoms, "all")
|
|
25
|
+
|
|
26
|
+
assert len(result) == 3
|
|
27
|
+
assert np.array_equal(result, np.array([0, 1, 2]))
|
|
28
|
+
|
|
29
|
+
def test_indices_none_returns_all(self):
|
|
30
|
+
"""Test None returns all atoms."""
|
|
31
|
+
atoms = Atoms('H4O2', positions=np.random.rand(6, 3))
|
|
32
|
+
result = indices(atoms, None)
|
|
33
|
+
|
|
34
|
+
assert len(result) == 6
|
|
35
|
+
assert np.array_equal(result, np.arange(6))
|
|
36
|
+
|
|
37
|
+
def test_indices_single_integer(self):
|
|
38
|
+
"""Test single integer."""
|
|
39
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
40
|
+
result = indices(atoms, 1)
|
|
41
|
+
|
|
42
|
+
assert len(result) == 1
|
|
43
|
+
assert result[0] == 1
|
|
44
|
+
|
|
45
|
+
def test_indices_list_integers(self):
|
|
46
|
+
"""Test list of integers."""
|
|
47
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
48
|
+
result = indices(atoms, [0, 2])
|
|
49
|
+
|
|
50
|
+
assert len(result) == 2
|
|
51
|
+
assert np.array_equal(result, np.array([0, 2]))
|
|
52
|
+
|
|
53
|
+
def test_indices_single_symbol(self):
|
|
54
|
+
"""Test single chemical symbol."""
|
|
55
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
56
|
+
result = indices(atoms, 'H')
|
|
57
|
+
|
|
58
|
+
assert len(result) == 2
|
|
59
|
+
|
|
60
|
+
def test_indices_list_symbols(self):
|
|
61
|
+
"""Test list of symbols."""
|
|
62
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
63
|
+
result = indices(atoms, ['H', 'O'])
|
|
64
|
+
|
|
65
|
+
assert len(result) == 3
|
|
66
|
+
|
|
67
|
+
def test_indices_oxygen_only(self):
|
|
68
|
+
"""Test getting oxygen only."""
|
|
69
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
70
|
+
result = indices(atoms, 'O')
|
|
71
|
+
|
|
72
|
+
assert len(result) == 1
|
|
73
|
+
|
|
74
|
+
def test_indices_invalid_raises_error(self):
|
|
75
|
+
"""Test invalid index raises error."""
|
|
76
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
77
|
+
|
|
78
|
+
with pytest.raises(ValueError):
|
|
79
|
+
indices(atoms, {'dict': 'value'})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestCoordinationFrame:
|
|
83
|
+
"""Test single frame coordination calculation."""
|
|
84
|
+
|
|
85
|
+
def test_coordination_frame_basic(self):
|
|
86
|
+
"""Test basic coordination frame calculation."""
|
|
87
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
88
|
+
atoms.cell = [10, 10, 10]
|
|
89
|
+
atoms.pbc = False
|
|
90
|
+
|
|
91
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
92
|
+
|
|
93
|
+
assert isinstance(result, dict)
|
|
94
|
+
assert len(result) > 0
|
|
95
|
+
assert all(isinstance(v, (int, np.integer)) for v in result.values())
|
|
96
|
+
|
|
97
|
+
def test_coordination_frame_with_indices(self):
|
|
98
|
+
"""Test coordination with index specifiers."""
|
|
99
|
+
atoms = Atoms('H4O2', positions=np.random.rand(6, 3) * 2)
|
|
100
|
+
atoms.cell = [10, 10, 10]
|
|
101
|
+
|
|
102
|
+
result = coordination_frame(
|
|
103
|
+
atoms,
|
|
104
|
+
central_atoms=[0, 1], # First two atoms as central
|
|
105
|
+
target_atoms=[2, 3, 4, 5] # Rest as target
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
assert isinstance(result, dict)
|
|
109
|
+
assert len(result) == 2
|
|
110
|
+
|
|
111
|
+
def test_coordination_frame_with_custom_cutoffs(self):
|
|
112
|
+
"""Test with custom cutoff distances."""
|
|
113
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
114
|
+
atoms.cell = [10, 10, 10]
|
|
115
|
+
|
|
116
|
+
custom_cutoffs = {('O', 'H'): 1.5}
|
|
117
|
+
|
|
118
|
+
result = coordination_frame(
|
|
119
|
+
atoms,
|
|
120
|
+
central_atoms='O',
|
|
121
|
+
target_atoms='H',
|
|
122
|
+
custom_cutoffs=custom_cutoffs
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
assert isinstance(result, dict)
|
|
126
|
+
|
|
127
|
+
def test_coordination_frame_with_mic(self):
|
|
128
|
+
"""Test with minimum image convention."""
|
|
129
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
130
|
+
atoms.cell = [10, 10, 10]
|
|
131
|
+
atoms.pbc = True
|
|
132
|
+
|
|
133
|
+
result_mic = coordination_frame(
|
|
134
|
+
atoms,
|
|
135
|
+
central_atoms='O',
|
|
136
|
+
target_atoms='H',
|
|
137
|
+
mic=True
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
result_no_mic = coordination_frame(
|
|
141
|
+
atoms,
|
|
142
|
+
central_atoms='O',
|
|
143
|
+
target_atoms='H',
|
|
144
|
+
mic=False
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert isinstance(result_mic, dict)
|
|
148
|
+
assert isinstance(result_no_mic, dict)
|
|
149
|
+
|
|
150
|
+
def test_coordination_frame_single_central_atom(self):
|
|
151
|
+
"""Test with single central atom."""
|
|
152
|
+
atoms = Atoms('H3O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]])
|
|
153
|
+
atoms.cell = [10, 10, 10]
|
|
154
|
+
|
|
155
|
+
result = coordination_frame(atoms, central_atoms=3, target_atoms='H')
|
|
156
|
+
|
|
157
|
+
assert len(result) == 1
|
|
158
|
+
assert 3 in result
|
|
159
|
+
|
|
160
|
+
def test_coordination_frame_multiple_central_atoms(self):
|
|
161
|
+
"""Test with multiple central atoms."""
|
|
162
|
+
atoms = Atoms('H4O2', positions=np.random.rand(6, 3) * 2)
|
|
163
|
+
atoms.cell = [10, 10, 10]
|
|
164
|
+
|
|
165
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
166
|
+
|
|
167
|
+
assert isinstance(result, dict)
|
|
168
|
+
assert all(isinstance(k, (int, np.integer)) for k in result.keys())
|
|
169
|
+
|
|
170
|
+
def test_coordination_frame_zero_coordination(self):
|
|
171
|
+
"""Test with isolated atoms (zero coordination)."""
|
|
172
|
+
atoms = Atoms('HO', positions=[[0, 0, 0], [100, 100, 100]])
|
|
173
|
+
atoms.cell = [200, 200, 200]
|
|
174
|
+
|
|
175
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
176
|
+
|
|
177
|
+
# Result should be a dict, values should be 0 (isolated atom)
|
|
178
|
+
assert isinstance(result, dict)
|
|
179
|
+
if len(result) > 0:
|
|
180
|
+
assert all(v == 0 for v in result.values())
|
|
181
|
+
|
|
182
|
+
def test_coordination_frame_identical_central_and_target(self):
|
|
183
|
+
"""Test when central and target atoms are the same."""
|
|
184
|
+
atoms = Atoms('O2', positions=[[0, 0, 0], [1.5, 0, 0]])
|
|
185
|
+
atoms.cell = [10, 10, 10]
|
|
186
|
+
|
|
187
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='O')
|
|
188
|
+
|
|
189
|
+
assert isinstance(result, dict)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestCoordinationTrajectory:
|
|
193
|
+
"""Test trajectory-based coordination analysis."""
|
|
194
|
+
|
|
195
|
+
@pytest.fixture
|
|
196
|
+
def coordination_trajectory(self):
|
|
197
|
+
"""Create trajectory for coordination tests."""
|
|
198
|
+
temp_dir = tempfile.mkdtemp()
|
|
199
|
+
traj_path = os.path.join(temp_dir, 'coord_traj.traj')
|
|
200
|
+
|
|
201
|
+
frames = []
|
|
202
|
+
for i in range(5):
|
|
203
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
204
|
+
atoms.cell = [10, 10, 10]
|
|
205
|
+
atoms.pbc = True
|
|
206
|
+
atoms.positions += np.random.rand(3, 3) * 0.2
|
|
207
|
+
frames.append(atoms)
|
|
208
|
+
|
|
209
|
+
write(traj_path, frames)
|
|
210
|
+
yield traj_path
|
|
211
|
+
shutil.rmtree(temp_dir)
|
|
212
|
+
|
|
213
|
+
def test_coordination_trajectory_basic(self, coordination_trajectory):
|
|
214
|
+
"""Test basic trajectory analysis."""
|
|
215
|
+
result = coordination(
|
|
216
|
+
coordination_trajectory,
|
|
217
|
+
central_atoms='O',
|
|
218
|
+
target_atoms='H',
|
|
219
|
+
custom_cutoffs=None,
|
|
220
|
+
frame_skip=1
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
assert result is not None
|
|
224
|
+
|
|
225
|
+
def test_coordination_frame_skip(self, coordination_trajectory):
|
|
226
|
+
"""Test with frame skipping."""
|
|
227
|
+
result = coordination(
|
|
228
|
+
coordination_trajectory,
|
|
229
|
+
central_atoms='O',
|
|
230
|
+
target_atoms='H',
|
|
231
|
+
custom_cutoffs=None,
|
|
232
|
+
frame_skip=2
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
assert result is not None
|
|
236
|
+
|
|
237
|
+
def test_coordination_custom_cutoff(self, coordination_trajectory):
|
|
238
|
+
"""Test with custom cutoff."""
|
|
239
|
+
result = coordination(
|
|
240
|
+
coordination_trajectory,
|
|
241
|
+
central_atoms='O',
|
|
242
|
+
target_atoms='H',
|
|
243
|
+
custom_cutoffs={('O', 'H'): 1.5},
|
|
244
|
+
frame_skip=1
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
assert result is not None
|
|
248
|
+
|
|
249
|
+
def test_coordination_nonexistent_file(self):
|
|
250
|
+
"""Test with non-existent file."""
|
|
251
|
+
with pytest.raises((FileNotFoundError, Exception)):
|
|
252
|
+
coordination('/nonexistent/path.traj', 'O', 'H', None)
|
|
253
|
+
|
|
254
|
+
def test_coordination_with_output_dir(self, coordination_trajectory):
|
|
255
|
+
"""Test with output directory."""
|
|
256
|
+
output_dir = tempfile.mkdtemp()
|
|
257
|
+
|
|
258
|
+
result = coordination(
|
|
259
|
+
coordination_trajectory,
|
|
260
|
+
central_atoms='O',
|
|
261
|
+
target_atoms='H',
|
|
262
|
+
custom_cutoffs=None,
|
|
263
|
+
output_dir=output_dir
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
assert result is not None
|
|
267
|
+
shutil.rmtree(output_dir)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TestCoordinationParameterVariations:
|
|
271
|
+
"""Test coordination analysis with various parameters."""
|
|
272
|
+
|
|
273
|
+
@pytest.fixture
|
|
274
|
+
def multi_atom_trajectory(self):
|
|
275
|
+
"""Create trajectory with multiple atom types."""
|
|
276
|
+
temp_dir = tempfile.mkdtemp()
|
|
277
|
+
traj_path = os.path.join(temp_dir, 'multi_coord.traj')
|
|
278
|
+
|
|
279
|
+
frames = []
|
|
280
|
+
for i in range(4):
|
|
281
|
+
atoms = Atoms('H4O2N2', positions=np.random.rand(8, 3) * 3)
|
|
282
|
+
atoms.cell = [15, 15, 15]
|
|
283
|
+
atoms.pbc = True
|
|
284
|
+
frames.append(atoms)
|
|
285
|
+
|
|
286
|
+
write(traj_path, frames)
|
|
287
|
+
yield traj_path
|
|
288
|
+
shutil.rmtree(temp_dir)
|
|
289
|
+
|
|
290
|
+
def test_coordination_different_central_atoms(self, multi_atom_trajectory):
|
|
291
|
+
"""Test with different central atom types."""
|
|
292
|
+
for central in ['O', 'N', 'H']:
|
|
293
|
+
result = coordination(
|
|
294
|
+
multi_atom_trajectory,
|
|
295
|
+
central_atoms=central,
|
|
296
|
+
target_atoms='H',
|
|
297
|
+
custom_cutoffs=None
|
|
298
|
+
)
|
|
299
|
+
assert result is not None
|
|
300
|
+
|
|
301
|
+
def test_coordination_different_target_atoms(self, multi_atom_trajectory):
|
|
302
|
+
"""Test with different target atom types."""
|
|
303
|
+
for target in ['O', 'N', 'H']:
|
|
304
|
+
result = coordination(
|
|
305
|
+
multi_atom_trajectory,
|
|
306
|
+
central_atoms='O',
|
|
307
|
+
target_atoms=target,
|
|
308
|
+
custom_cutoffs=None
|
|
309
|
+
)
|
|
310
|
+
assert result is not None
|
|
311
|
+
|
|
312
|
+
def test_coordination_multiple_target_atoms(self, multi_atom_trajectory):
|
|
313
|
+
"""Test with multiple target atom types."""
|
|
314
|
+
result = coordination(
|
|
315
|
+
multi_atom_trajectory,
|
|
316
|
+
central_atoms='O',
|
|
317
|
+
target_atoms=['H', 'N'],
|
|
318
|
+
custom_cutoffs=None
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
assert result is not None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class TestCoordinationIntegration:
|
|
325
|
+
"""Integration tests for coordination analysis."""
|
|
326
|
+
|
|
327
|
+
def test_coordination_workflow_water_network(self):
|
|
328
|
+
"""Test coordination in water-like network."""
|
|
329
|
+
# Create water-like network
|
|
330
|
+
positions = [
|
|
331
|
+
[0, 0, 0], [1, 0, 0], [0, 1, 0], # Water 1
|
|
332
|
+
[3, 0, 0], [4, 0, 0], [3, 1, 0], # Water 2
|
|
333
|
+
[1.5, 1.5, 0], [2.5, 1.5, 0], [1.5, 2.5, 0], # Water 3
|
|
334
|
+
]
|
|
335
|
+
atoms = Atoms('H2OH2OH2O', positions=positions)
|
|
336
|
+
atoms.cell = [10, 10, 10]
|
|
337
|
+
atoms.pbc = False
|
|
338
|
+
|
|
339
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
340
|
+
|
|
341
|
+
assert isinstance(result, dict)
|
|
342
|
+
assert all(isinstance(v, (int, np.integer)) for v in result.values())
|
|
343
|
+
|
|
344
|
+
def test_coordination_frame_consistency(self):
|
|
345
|
+
"""Test that same frame gives same coordination."""
|
|
346
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
347
|
+
atoms.cell = [10, 10, 10]
|
|
348
|
+
|
|
349
|
+
result1 = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
350
|
+
result2 = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
351
|
+
|
|
352
|
+
assert result1 == result2
|
|
353
|
+
|
|
354
|
+
def test_coordination_with_custom_indices(self):
|
|
355
|
+
"""Test coordination using custom atom indices."""
|
|
356
|
+
atoms = Atoms('H4O2', positions=np.random.rand(6, 3) * 2)
|
|
357
|
+
atoms.cell = [10, 10, 10]
|
|
358
|
+
|
|
359
|
+
# Get H atoms via indices function
|
|
360
|
+
h_indices = indices(atoms, 'H')
|
|
361
|
+
o_indices = indices(atoms, 'O')
|
|
362
|
+
|
|
363
|
+
# Convert numpy int64 to regular Python int to avoid type issues
|
|
364
|
+
result = coordination_frame(atoms, central_atoms=[int(i) for i in o_indices], target_atoms=[int(i) for i in h_indices])
|
|
365
|
+
|
|
366
|
+
assert isinstance(result, dict)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class TestCoordinationEdgeCases:
|
|
370
|
+
"""Test edge cases and error handling."""
|
|
371
|
+
|
|
372
|
+
def test_coordination_single_atom(self):
|
|
373
|
+
"""Test with single atom."""
|
|
374
|
+
atoms = Atoms('O', positions=[[0, 0, 0]])
|
|
375
|
+
|
|
376
|
+
result = coordination_frame(atoms, central_atoms=0, target_atoms='H')
|
|
377
|
+
|
|
378
|
+
assert 0 in result
|
|
379
|
+
assert result[0] == 0
|
|
380
|
+
|
|
381
|
+
def test_coordination_isolated_atoms(self):
|
|
382
|
+
"""Test with isolated atoms."""
|
|
383
|
+
atoms = Atoms('HO', positions=[[0, 0, 0], [100, 100, 100]])
|
|
384
|
+
atoms.cell = [200, 200, 200]
|
|
385
|
+
|
|
386
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
387
|
+
|
|
388
|
+
# Oxygen should have 0 coordination
|
|
389
|
+
assert all(v == 0 for v in result.values()) or len(result) == 0
|
|
390
|
+
|
|
391
|
+
def test_coordination_no_target_atoms(self):
|
|
392
|
+
"""Test when target atoms don't exist."""
|
|
393
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
394
|
+
atoms.cell = [10, 10, 10]
|
|
395
|
+
|
|
396
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='X')
|
|
397
|
+
|
|
398
|
+
# Should handle gracefully
|
|
399
|
+
assert isinstance(result, dict)
|
|
400
|
+
|
|
401
|
+
def test_coordination_very_large_system(self):
|
|
402
|
+
"""Test with large system."""
|
|
403
|
+
symbols = ['H'] * 50 + ['O'] * 50
|
|
404
|
+
positions = np.random.rand(100, 3) * 10
|
|
405
|
+
atoms = Atoms(symbols, positions=positions)
|
|
406
|
+
atoms.cell = [20, 20, 20]
|
|
407
|
+
atoms.pbc = True
|
|
408
|
+
|
|
409
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
410
|
+
|
|
411
|
+
assert isinstance(result, dict)
|
|
412
|
+
assert len(result) == 50 # 50 oxygen atoms
|
|
413
|
+
|
|
414
|
+
def test_coordination_periodic_boundaries(self):
|
|
415
|
+
"""Test coordination across periodic boundaries."""
|
|
416
|
+
atoms = Atoms('HO', positions=[[0, 0, 0], [9.9, 0, 0]])
|
|
417
|
+
atoms.cell = [10, 10, 10]
|
|
418
|
+
atoms.pbc = True
|
|
419
|
+
|
|
420
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H', mic=True)
|
|
421
|
+
|
|
422
|
+
# With MIC, the H and O should be close
|
|
423
|
+
assert isinstance(result, dict)
|
|
424
|
+
|
|
425
|
+
def test_coordination_very_close_atoms(self):
|
|
426
|
+
"""Test with very close atoms."""
|
|
427
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [0.001, 0, 0], [0.002, 0, 0]])
|
|
428
|
+
atoms.cell = [10, 10, 10]
|
|
429
|
+
|
|
430
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
431
|
+
|
|
432
|
+
assert isinstance(result, dict)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class TestCoordinationNumericalStability:
|
|
436
|
+
"""Test numerical stability of coordination calculations."""
|
|
437
|
+
|
|
438
|
+
def test_coordination_with_identical_positions(self):
|
|
439
|
+
"""Test with atoms at identical positions."""
|
|
440
|
+
atoms = Atoms('HO', positions=[[0, 0, 0], [0, 0, 0]])
|
|
441
|
+
atoms.cell = [10, 10, 10]
|
|
442
|
+
|
|
443
|
+
# Should handle gracefully
|
|
444
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
445
|
+
assert isinstance(result, dict)
|
|
446
|
+
|
|
447
|
+
def test_coordination_with_extreme_cell_size(self):
|
|
448
|
+
"""Test with very large cell."""
|
|
449
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [1, 0, 0], [0, 1, 0]])
|
|
450
|
+
atoms.cell = [10000, 10000, 10000]
|
|
451
|
+
atoms.pbc = True
|
|
452
|
+
|
|
453
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H')
|
|
454
|
+
|
|
455
|
+
assert isinstance(result, dict)
|
|
456
|
+
|
|
457
|
+
def test_coordination_with_small_cell(self):
|
|
458
|
+
"""Test with very small cell (but valid)."""
|
|
459
|
+
atoms = Atoms('H2O', positions=[[0, 0, 0], [0.5, 0, 0], [0, 0.5, 0]])
|
|
460
|
+
atoms.cell = [1, 1, 1]
|
|
461
|
+
atoms.pbc = True
|
|
462
|
+
|
|
463
|
+
result = coordination_frame(atoms, central_atoms='O', target_atoms='H', mic=True)
|
|
464
|
+
|
|
465
|
+
assert isinstance(result, dict)
|