Skip to content

Commit 34f1177

Browse files
committed
Unify assign_site_occupations signature to accept lattice_matrix
Update the abstract SiteCollection.assign_site_occupations() and all implementations to accept lattice_matrix (numpy array) instead of Structure. Trajectory.assign_site_occupations() extracts structure.lattice.matrix at the boundary before delegating. PolyhedralSiteCollection accepts lattice_matrix for interface consistency (not currently used for containment checks).
1 parent 0a32487 commit 34f1177

8 files changed

Lines changed: 53 additions & 55 deletions

site_analysis/polyhedral_site_collection.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,19 @@ def analyse_structure(self,
7070
lattice_matrix = structure.lattice.matrix
7171
for s in self.sites:
7272
s.notify_structure_changed(all_frac_coords, lattice_matrix)
73-
self.assign_site_occupations(atoms, structure)
74-
75-
def assign_site_occupations(self, atoms, structure) -> None:
73+
self.assign_site_occupations(atoms, lattice_matrix)
74+
75+
def assign_site_occupations(self, atoms, lattice_matrix) -> None:
7676
"""Assign atoms to polyhedral sites based on their positions.
77-
77+
7878
This method implements an optimised assignment logic using a priority-based
7979
site checking approach.
80-
80+
8181
Args:
82-
atoms: List of Atom objects to be assigned to sites
83-
structure: Pymatgen Structure containing the atom positions
82+
atoms: List of Atom objects to be assigned to sites.
83+
lattice_matrix: (3, 3) lattice matrix where rows are lattice
84+
vectors. Not currently used for polyhedral containment
85+
checks, but accepted for interface consistency.
8486
"""
8587
self.reset_site_occupations()
8688
for atom in atoms:

site_analysis/site_collection.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,17 @@ def __init__(self, sites: Sequence[Site]) -> None:
210210
self._site_lookup[site.index] = site
211211

212212
@abstractmethod
213-
def assign_site_occupations(self, atoms, structure):
214-
"""Assigns atoms to sites for a specific structure.
215-
216-
This method should be implemented in the derived subclass
213+
def assign_site_occupations(self, atoms, lattice_matrix):
214+
"""Assign atoms to sites.
217215
218216
Args:
219-
atoms (list(Atom)): List of Atom objects to be assigned to sites.
220-
struture (pymatgen.Structure): Pymatgen Structure object used to specificy
221-
the atomic coordinates.
222-
223-
Returns:
224-
None
225-
226-
Notes:
227-
The atom coordinates should already be consistent with the coordinates
228-
in `structure`. Recommended usage is via the ``analyse_structure()`` method.
217+
atoms: List of Atom objects to be assigned to sites.
218+
lattice_matrix: (3, 3) lattice matrix where rows are lattice
219+
vectors.
229220
221+
Note:
222+
The atom coordinates should already be consistent with the
223+
structure. Recommended usage is via ``analyse_structure()``.
230224
"""
231225
raise NotImplementedError('assign_site_occupations should be implemented in'
232226
' the derived class')

site_analysis/trajectory.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,13 @@ def analyse_structure(self,
142142
def assign_site_occupations(self,
143143
structure: Structure) -> None:
144144
"""Assign atoms to sites for a specific structure.
145-
145+
146146
This delegates the assignment to the site collection's assign_site_occupations method.
147-
147+
148148
Args:
149149
structure: A pymatgen Structure object to be analysed.
150150
"""
151-
self.site_collection.assign_site_occupations(self.atoms, structure)
151+
self.site_collection.assign_site_occupations(self.atoms, structure.lattice.matrix)
152152

153153
def site_coordination_numbers(self) -> Counter:
154154
"""Return the coordination numbers of all sites.

tests/benchmark_containment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def _eager_analyse_structure(self, atoms, structure):
8181
a.assign_coords(frac_coords)
8282
for s in self.sites:
8383
s.assign_vertex_coords(structure)
84-
self.assign_site_occupations(atoms, structure)
84+
self.assign_site_occupations(atoms, structure.lattice.matrix)
8585

8686

8787
def benchmark_assignment(trajectory, structures, repeats):

tests/test_polyhedral_site_collection.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,11 @@ def test_analyse_structure(self):
108108
# Verify each site was notified of the new structure
109109
self.assertEqual(mock_notify.call_count, 3)
110110

111-
# Verify assign_site_occupations was called with atoms and structure
112-
mock_assign.assert_called_once_with(self.atoms, self.structure)
111+
# Verify assign_site_occupations was called with atoms and lattice_matrix
112+
mock_assign.assert_called_once()
113+
args = mock_assign.call_args[0]
114+
self.assertIs(args[0], self.atoms)
115+
np.testing.assert_array_equal(args[1], self.structure.lattice.matrix)
113116

114117
def test_assign_site_occupations_atom_in_site(self):
115118
"""Test assign_site_occupations when atoms are already in sites."""
@@ -126,7 +129,7 @@ def test_assign_site_occupations_atom_in_site(self):
126129
mock_contains_atom.return_value = True
127130

128131
# Call method
129-
self.collection.assign_site_occupations(self.atoms, self.structure)
132+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
130133

131134
# Verify site occupations were reset
132135
self.assertEqual(self.site1.contains_atoms, [])
@@ -154,7 +157,7 @@ def test_assign_site_occupations_atom_moved(self):
154157
mock_contains_atom.return_value = False
155158

156159
# Call method
157-
self.collection.assign_site_occupations(self.atoms, self.structure)
160+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
158161

159162
# Verify site occupations were reset
160163
self.assertEqual(self.site1.contains_atoms, [])
@@ -184,7 +187,7 @@ def test_assign_site_occupations_atom_not_in_site(self):
184187
mock_contains_atom.side_effect = lambda atom, **kwargs: atom is self.atom1 and mock_contains_atom.mock_calls[0][1][0] is atom
185188

186189
# Call method
187-
self.collection.assign_site_occupations(self.atoms, self.structure)
190+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
188191

189192
# Verify site occupations were reset
190193
self.assertEqual(self.site1.contains_atoms, [])
@@ -217,7 +220,7 @@ def test_empty_atoms_list_polyhedral(self):
217220
)
218221

219222
# Call the method with empty atom list
220-
collection.assign_site_occupations([], structure)
223+
collection.assign_site_occupations([], lattice.matrix)
221224

222225
# Verify that contains_atoms was reset for both sites
223226
self.assertEqual(site1.contains_atoms, [])
@@ -311,7 +314,7 @@ def test_checks_recent_site_via_priority_heuristic(self):
311314
patch.object(collection, 'site_by_index') as mock_site_by_index:
312315
mock_site_by_index.return_value = mock_site
313316

314-
collection.assign_site_occupations([mock_atom], mock_structure)
317+
collection.assign_site_occupations([mock_atom], np.eye(3) * 10.0)
315318

316319
mock_site_by_index.assert_called_with(5)
317320
mock_update.assert_called_with(mock_site, mock_atom)
@@ -501,7 +504,7 @@ def setUp(self):
501504
def test_calls_generator_for_each_atom(self):
502505
"""Test that _get_priority_sites is called once per atom."""
503506
with patch.object(self.collection, '_get_priority_sites', return_value=[]):
504-
self.collection.assign_site_occupations(self.atoms, self.structure)
507+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
505508
self.collection._get_priority_sites.assert_called_once_with(self.atom)
506509

507510
def test_calls_generator_for_multiple_atoms(self):
@@ -511,7 +514,7 @@ def test_calls_generator_for_multiple_atoms(self):
511514
atoms = atoms_from_structure(self.structure, "Li")
512515

513516
with patch.object(self.collection, '_get_priority_sites', return_value=[]):
514-
self.collection.assign_site_occupations(atoms, self.structure)
517+
self.collection.assign_site_occupations(atoms, self.lattice.matrix)
515518
self.assertEqual(self.collection._get_priority_sites.call_count, 2)
516519

517520
def test_checks_sites_in_generator_order(self):
@@ -523,7 +526,7 @@ def test_checks_sites_in_generator_order(self):
523526
with patch.object(self.collection, '_get_priority_sites') as mock_gen:
524527
mock_gen.return_value = [self.site2, self.site1] # site2 first
525528

526-
self.collection.assign_site_occupations(self.atoms, self.structure)
529+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
527530

528531
self.assertEqual(call_order, [2]) # Only site2 checked (found there)
529532

@@ -535,7 +538,7 @@ def test_stops_checking_when_atom_found(self):
535538
with patch.object(self.collection, '_get_priority_sites') as mock_gen:
536539
mock_gen.return_value = [self.site1, self.site2]
537540

538-
self.collection.assign_site_occupations(self.atoms, self.structure)
541+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
539542

540543
self.site1.contains_atom.assert_called_once()
541544
self.site2.contains_atom.assert_not_called()
@@ -546,7 +549,7 @@ def test_calls_update_occupation_when_found(self):
546549

547550
with patch.object(self.collection, '_get_priority_sites', return_value=[self.site1]):
548551
with patch.object(self.collection, 'update_occupation') as mock_update:
549-
self.collection.assign_site_occupations(self.atoms, self.structure)
552+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
550553
mock_update.assert_called_once_with(self.site1, self.atom)
551554

552555
def test_handles_atom_not_found(self):
@@ -555,7 +558,7 @@ def test_handles_atom_not_found(self):
555558

556559
with patch.object(self.collection, '_get_priority_sites', return_value=[self.site1]):
557560
with patch.object(self.collection, 'update_occupation') as mock_update:
558-
self.collection.assign_site_occupations(self.atoms, self.structure)
561+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
559562

560563
mock_update.assert_not_called()
561564
self.assertIsNone(self.atom.in_site)
@@ -564,15 +567,15 @@ def test_resets_site_occupations(self):
564567
"""Test that reset_site_occupations is called at start."""
565568
with patch.object(self.collection, 'reset_site_occupations') as mock_reset:
566569
with patch.object(self.collection, '_get_priority_sites', return_value=[]):
567-
self.collection.assign_site_occupations(self.atoms, self.structure)
570+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
568571
mock_reset.assert_called_once()
569572

570573
def test_resets_atom_in_site(self):
571574
"""Test that atom.in_site is reset to None."""
572575
self.atom.in_site = 999 # Set to some previous value
573576

574577
with patch.object(self.collection, '_get_priority_sites', return_value=[]):
575-
self.collection.assign_site_occupations(self.atoms, self.structure)
578+
self.collection.assign_site_occupations(self.atoms, self.lattice.matrix)
576579
self.assertIsNone(self.atom.in_site)
577580

578581

tests/test_site_collection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ def test_assign_site_occupations_raises_not_implemented_error(self):
3333
Mock(spec=Site, index=1)]
3434
atoms = [Mock(spec=Atom),
3535
Mock(spec=Atom)]
36-
structure = Mock(spec=Structure)
36+
lattice_matrix = np.eye(3) * 10.0
3737
site_collection = ConcreteSiteCollection(sites=sites)
3838
with self.assertRaises(NotImplementedError):
39-
site_collection.assign_site_occupations(atoms, structure)
39+
site_collection.assign_site_occupations(atoms, lattice_matrix)
4040

4141
def test_analyse_structure_raises_not_implemented_error(self):
4242
sites = [Mock(spec=Site, index=0),

tests/test_trajectory.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -307,20 +307,15 @@ def test_site_labels(self):
307307
self.assertEqual(labels[1], "site2")
308308

309309
def test_assign_site_occupations(self):
310-
"""Test that assign_site_occupations delegates to site_collection."""
311-
# Setup
312-
structure = Mock()
313-
314-
# Mock the site_collection
310+
"""Test that assign_site_occupations extracts lattice_matrix and delegates."""
315311
self.trajectory.site_collection = Mock()
316-
317-
# Call the method
318-
self.trajectory.assign_site_occupations(structure)
319-
320-
# Check delegation
321-
self.trajectory.site_collection.assign_site_occupations.assert_called_once_with(
322-
self.atoms, structure
323-
)
312+
313+
self.trajectory.assign_site_occupations(self.structure)
314+
315+
self.trajectory.site_collection.assign_site_occupations.assert_called_once()
316+
args = self.trajectory.site_collection.assign_site_occupations.call_args[0]
317+
self.assertIs(args[0], self.atoms)
318+
np.testing.assert_array_equal(args[1], self.structure.lattice.matrix)
324319

325320
def test_trajectory_from_structures_with_progress(self):
326321
"""Test trajectory_from_structures wraps iterator with tqdm."""

tests/test_voronoi_site_collection.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ def test_assign_site_occupations_distance_matrix(self):
9090
self.atoms, self.structure.lattice.matrix)
9191

9292
mock_distances.assert_called_once()
93+
args = mock_distances.call_args[0]
94+
np.testing.assert_array_equal(args[0], np.array([s.frac_coords for s in self.collection.sites]))
95+
np.testing.assert_array_equal(args[1], np.array([a.frac_coords for a in self.atoms]))
96+
np.testing.assert_array_equal(args[2], self.structure.lattice.matrix)
9397
mock_update.assert_any_call(self.site1, self.atom1)
9498
mock_update.assert_any_call(self.site2, self.atom2)
9599
self.assertEqual(mock_update.call_count, 2)

0 commit comments

Comments
 (0)