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.
Files changed (42) hide show
  1. CRISP/__init__.py +99 -0
  2. CRISP/_version.py +1 -0
  3. CRISP/cli.py +41 -0
  4. CRISP/data_analysis/__init__.py +38 -0
  5. CRISP/data_analysis/clustering.py +838 -0
  6. CRISP/data_analysis/contact_coordination.py +915 -0
  7. CRISP/data_analysis/h_bond.py +772 -0
  8. CRISP/data_analysis/msd.py +1199 -0
  9. CRISP/data_analysis/prdf.py +404 -0
  10. CRISP/data_analysis/volumetric_atomic_density.py +527 -0
  11. CRISP/py.typed +1 -0
  12. CRISP/simulation_utility/__init__.py +31 -0
  13. CRISP/simulation_utility/atomic_indices.py +155 -0
  14. CRISP/simulation_utility/atomic_traj_linemap.py +278 -0
  15. CRISP/simulation_utility/error_analysis.py +254 -0
  16. CRISP/simulation_utility/interatomic_distances.py +200 -0
  17. CRISP/simulation_utility/subsampling.py +241 -0
  18. CRISP/tests/DataAnalysis/__init__.py +1 -0
  19. CRISP/tests/DataAnalysis/test_clustering_extended.py +212 -0
  20. CRISP/tests/DataAnalysis/test_contact_coordination.py +184 -0
  21. CRISP/tests/DataAnalysis/test_contact_coordination_extended.py +465 -0
  22. CRISP/tests/DataAnalysis/test_h_bond_complete.py +326 -0
  23. CRISP/tests/DataAnalysis/test_h_bond_extended.py +322 -0
  24. CRISP/tests/DataAnalysis/test_msd_complete.py +305 -0
  25. CRISP/tests/DataAnalysis/test_msd_extended.py +522 -0
  26. CRISP/tests/DataAnalysis/test_prdf.py +206 -0
  27. CRISP/tests/DataAnalysis/test_volumetric_atomic_density.py +463 -0
  28. CRISP/tests/SimulationUtility/__init__.py +1 -0
  29. CRISP/tests/SimulationUtility/test_atomic_traj_linemap.py +101 -0
  30. CRISP/tests/SimulationUtility/test_atomic_traj_linemap_extended.py +469 -0
  31. CRISP/tests/SimulationUtility/test_error_analysis_extended.py +151 -0
  32. CRISP/tests/SimulationUtility/test_interatomic_distances.py +223 -0
  33. CRISP/tests/SimulationUtility/test_subsampling.py +365 -0
  34. CRISP/tests/__init__.py +1 -0
  35. CRISP/tests/test_CRISP.py +28 -0
  36. CRISP/tests/test_cli.py +87 -0
  37. CRISP/tests/test_crisp_comprehensive.py +679 -0
  38. crisp_ase-1.1.2.dist-info/METADATA +141 -0
  39. crisp_ase-1.1.2.dist-info/RECORD +42 -0
  40. crisp_ase-1.1.2.dist-info/WHEEL +5 -0
  41. crisp_ase-1.1.2.dist-info/entry_points.txt +2 -0
  42. 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)