Skip to content

Commit 3c8d92d

Browse files
Merge pull request #33 from AsymmetryChou/sort_bug
Fix atom sorting and desync issue with unit test
2 parents 560b47f + 9018c45 commit 3c8d92d

2 files changed

Lines changed: 90 additions & 3 deletions

File tree

dpnegf/negf/negf_hamiltonian_init.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,7 @@ def __init__(self,
122122

123123
# sort the atoms in device region lexicographically
124124
if block_tridiagonal:
125-
self.structase.positions[self.device_id[0]:self.device_id[1]] =\
126-
self.structase.positions[self.device_id[0]:self.device_id[1]][sort_lexico(self.structase.positions[self.device_id[0]:self.device_id[1]])]
125+
self.structase = self._sort_device_lexico(self.structase, self.device_id)
127126
log.info(msg="The structure is sorted lexicographically in this version!")
128127

129128
if self.unit == "Hartree":
@@ -142,6 +141,41 @@ def __init__(self,
142141
atom_norbs.append(int(self.model.idp.atom_norb[model.idp.chemical_symbol_to_type[atom.symbol]]))
143142
self.atom_norbs = atom_norbs
144143

144+
@staticmethod
145+
def _sort_device_lexico(structase, device_id):
146+
'''Lexicographically sort the atoms in the device region.
147+
148+
The device slice ``[device_id[0]:device_id[1]]`` is reordered using
149+
``sort_lexico`` on the atomic positions. Crucially, the whole atoms
150+
slice is permuted so that the atomic species (numbers) stay attached to
151+
their coordinates. Permuting only ``positions`` (as an earlier version
152+
did) desyncs positions and species and corrupts the Hamiltonian for any
153+
device that is not already lexicographically ordered, especially for
154+
multi-species devices.
155+
156+
Parameters
157+
----------
158+
structase : ase.Atoms
159+
The full structure (leads + device).
160+
device_id : list[int]
161+
``[start, end]`` atom indices delimiting the device region.
162+
163+
Returns
164+
-------
165+
ase.Atoms
166+
The structure with the device region sorted; lead regions untouched.
167+
'''
168+
d0, d1 = device_id[0], device_id[1]
169+
perm = sort_lexico(structase.positions[d0:d1])
170+
# Build global index order: leads verbatim, device permuted. Indexing an
171+
# Atoms object with an index array carries positions AND numbers together.
172+
order = np.concatenate([
173+
np.arange(d0),
174+
d0 + perm,
175+
np.arange(d1, len(structase)),
176+
])
177+
return structase[order]
178+
145179
def initialize(self, kpoints, block_tridiagnal=False, plot_blocks=False, \
146180
useBloch=False,bloch_factor=None,\
147181
use_saved_HS=False, saved_HS_path=None):

dpnegf/tests/test_negf_negf_hamiltonian_init.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,5 +317,58 @@ def test_calc_principal_layers_disp_vec():
317317
# assert na == 4
318318
# assert hamiltonian.device_norbs==device_norbs_standard
319319

320-
320+
def test_sort_device_lexico_preserves_species():
321+
"""Regression test for the device-sort species desync bug.
322+
323+
The block-tridiagonal device sort must permute the whole atoms slice so
324+
that atomic species stay attached to their coordinates. A previous version
325+
permuted only ``positions``, which for a non-pre-sorted, multi-species
326+
device relocated species onto the wrong sites and corrupted the
327+
Hamiltonian.
328+
"""
329+
from ase import Atoms
330+
from dpnegf.negf.negf_hamiltonian_init import NEGFHamiltonianInit
331+
from dpnegf.negf.sort_btd import sort_lexico
332+
333+
# leads: 2 C atoms each; device: mixed C/H that is NOT lexicographically
334+
# ordered along z, so a correct sort must actually permute it.
335+
# idx: 0 1 | 2 3 4 5 | 6 7
336+
# sym: C C | C H C H | C C
337+
# z : 0 1 | 5 3 4 2 | 8 9
338+
symbols = ["C", "C", "C", "H", "C", "H", "C", "C"]
339+
zs = [0.0, 1.0, 5.0, 3.0, 4.0, 2.0, 8.0, 9.0]
340+
positions = [[0.0, 0.0, z] for z in zs]
341+
atoms = Atoms(symbols=symbols, positions=positions,
342+
cell=[10.0, 10.0, 10.0], pbc=[True, True, False])
343+
device_id = [2, 6]
344+
345+
# ground-truth mapping computed independently
346+
dev_pos = np.array(positions)[device_id[0]:device_id[1]]
347+
perm = sort_lexico(dev_pos)
348+
expected_dev_sym = np.array(symbols[device_id[0]:device_id[1]])[perm].tolist()
349+
expected_dev_z = dev_pos[perm][:, 2].tolist()
350+
351+
sorted_atoms = NEGFHamiltonianInit._sort_device_lexico(atoms, device_id)
352+
got_sym = list(sorted_atoms.get_chemical_symbols())
353+
got_z = sorted_atoms.positions[:, 2].tolist()
354+
355+
# device region: species stay attached to their (now sorted) coordinates
356+
assert got_sym[device_id[0]:device_id[1]] == expected_dev_sym
357+
assert np.allclose(got_z[device_id[0]:device_id[1]], expected_dev_z)
358+
# device coordinates are actually sorted ascending in z
359+
assert got_z[device_id[0]:device_id[1]] == sorted(got_z[device_id[0]:device_id[1]])
360+
# H atoms remain H at the correct sorted coordinates (z=2 and z=3)
361+
dev_pairs = list(zip(got_sym[device_id[0]:device_id[1]],
362+
got_z[device_id[0]:device_id[1]]))
363+
assert ("H", 2.0) in dev_pairs and ("H", 3.0) in dev_pairs
364+
365+
# leads are untouched (verbatim)
366+
assert got_sym[:device_id[0]] == symbols[:device_id[0]]
367+
assert got_sym[device_id[1]:] == symbols[device_id[1]:]
368+
assert np.allclose(got_z[:device_id[0]], zs[:device_id[0]])
369+
assert np.allclose(got_z[device_id[1]:], zs[device_id[1]:])
370+
371+
# cell and pbc preserved
372+
assert np.allclose(sorted_atoms.cell, atoms.cell)
373+
assert list(sorted_atoms.pbc) == list(atoms.pbc)
321374

0 commit comments

Comments
 (0)