Skip to content

Commit d858392

Browse files
committed
fix the inconsistency of atom order between ase and abacus
1 parent e820d74 commit d858392

File tree

6 files changed

+213
-23
lines changed

6 files changed

+213
-23
lines changed

python/ase-plugin/abacuslite/core.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ def __init__(self):
137137
self.outputname = f'{self._label}.out'
138138
self.errorname = f'{self._label}.err'
139139

140+
# fix: inconsistent atoms order may induce bugs, here a list
141+
# is kept to swap the order of atoms
142+
self.atomorder = None
143+
140144
'''because it may be not one-to-one mapping between the property
141145
desired to calculate and the keywords used in the calculation,
142146
in the following a series of functions for mapping the property
@@ -207,7 +211,7 @@ def counter(param_new: Dict[str, str]) -> Dict[str, str]:
207211
def write_input(self,
208212
profile: AbacusProfile,
209213
directory: Path | str,
210-
atoms,
214+
atoms: Atoms,
211215
parameters: Dict[str, str],
212216
properties: List[str]) -> None:
213217
'''Write the input files for the calculation. This function connects
@@ -236,7 +240,12 @@ def write_input(self,
236240

237241
# STRU
238242
_ = file_safe_backup(directory / parameters.get('stru_file', 'STRU'))
239-
_ = write_stru(atoms,
243+
# reorder the atoms according to the alphabet. Keep the reverse map
244+
# so that we will recover the order in function read_results()
245+
ind = sorted(range(len(atoms)), key=lambda i: atoms[i].symbol)
246+
self.atomorder = sorted(range(len(atoms)), key=lambda i: ind[i]) # revmap
247+
# then we write
248+
_ = write_stru(atoms[ind],
240249
outdir=directory,
241250
pp_file=parameters.get('pseudopotentials'),
242251
orb_file=parameters.get('basissets'),
@@ -292,10 +301,33 @@ def write_input(self,
292301
def execute(self,
293302
directory: Path | str,
294303
profile: AbacusProfile):
295-
profile.run(directory=directory,
296-
inputfile=None,
297-
outputfile=self.outputname,
298-
errorfile=self.errorname)
304+
'''Execute the ABACUS Lite calculation.
305+
306+
Parameters
307+
----------
308+
directory : Path or str
309+
The working directory to store the input files.
310+
profile : AbacusProfile
311+
The profile used to perform the calculation.
312+
313+
Raises
314+
------
315+
SubprocessError
316+
If the ABACUS Lite calculation fails.
317+
'''
318+
from subprocess import SubprocessError
319+
try:
320+
profile.run(directory=directory,
321+
inputfile=None,
322+
outputfile=self.outputname,
323+
errorfile=self.errorname)
324+
except SubprocessError:
325+
message = ['ABACUS Lite calculation failed']
326+
with open(directory / self.outputname, 'r') as f:
327+
message.append(f.read())
328+
with open(directory / self.errorname, 'r') as f:
329+
message.append(f.read())
330+
raise SubprocessError('\n'.join(message))
299331

300332
def read_results(self, directory) -> Dict:
301333
'''the function that returns the desired properties in dict'''
@@ -305,9 +337,14 @@ def read_results(self, directory) -> Dict:
305337
from abacuslite.io.legacyio import read_abacus_out
306338
else:
307339
from abacuslite.io.latestio import read_abacus_out
340+
308341
outdir = directory / f'OUT.{self.suffix}'
309-
atoms = read_abacus_out(outdir / f'running_{self.calculation}.log')[-1]
342+
# only the last frame
343+
atoms: Optional[Atoms] = read_abacus_out(
344+
outdir / f'running_{self.calculation}.log',
345+
sort_atoms_with=self.atomorder)[-1]
310346
assert atoms is not None
347+
311348
return dict(atoms.calc.properties())
312349

313350
def load_profile(self, cfg, **kwargs):

python/ase-plugin/abacuslite/io/latestio.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import shutil
44
import unittest
55
from pathlib import Path
6-
from typing import Dict, List
6+
from typing import Dict, List, Optional
77

88
import numpy as np
99
from ase.atoms import Atoms
@@ -359,8 +359,10 @@ def read_stress_from_running_log(src: str | Path | List[str]) \
359359

360360
return stresses
361361

362-
def read_abacus_out(fileobj, index=slice(None), results_required=True) \
363-
-> Atoms | List[Atoms]:
362+
def read_abacus_out(fileobj,
363+
index=slice(None),
364+
results_required=True,
365+
sort_atoms_with: Optional[List[int]] = None) -> Atoms | List[Atoms]:
364366
'''Reads the ABACUS output files. This function would be called by
365367
the AbacusTemplate.read_results() function. The detailed call stack
366368
is as follows:
@@ -385,6 +387,9 @@ def read_abacus_out(fileobj, index=slice(None), results_required=True) \
385387
Whether the results are required. If True, the results will be
386388
returned. If False, the results will not be returned. This parameter
387389
is not used.
390+
sort_atoms_with: Optional[List[int]]
391+
The sort order of the atoms. If not None, the atoms will be sorted
392+
according to the order in the list.
388393
389394
Returns
390395
-------
@@ -401,7 +406,9 @@ def read_abacus_out(fileobj, index=slice(None), results_required=True) \
401406

402407
# read the esolver type
403408
eslvtyp = read_esolver_type_from_running_log(abacus_lines)
404-
409+
# FIXME: implement read_ksdft_esolver_out instead of read_abacus_out to
410+
# make flexible esolver support
411+
405412
# read the structure, with the cell, elem, etc. (nframe)
406413
# if it is MD run, the trajectories will be in the file MD_dump, instead
407414
# of the running log
@@ -456,15 +463,18 @@ def read_abacus_out(fileobj, index=slice(None), results_required=True) \
456463
magmom = [np.zeros(shape=(len(trajectory[0]['elem'])))] * len(trajectory)
457464

458465
# loop over the frame...
459-
images = []
466+
images, ind = [], sort_atoms_with
460467
for frame, estat, mag, frs, strs, ener in zip(
461468
trajectory, elecstate, magmom, forces, stress, energies):
462469
# for each frame, a structure can be defined
463-
atoms = Atoms(symbols=frame['elem'], positions=frame['coords'], cell=frame['cell'])
470+
ind = ind or list(range(len(frame['elem'])))
471+
atoms = Atoms(symbols=np.array(frame['elem'])[ind].tolist(),
472+
positions=frame['coords'][ind],
473+
cell=frame['cell'])
464474
# from result, a calculator can be assembled
465475
# however, sometimes the force and stress is not calculated
466476
# in this case, we set them to None
467-
frs = None if is_invalid_arr(frs) else frs
477+
frs = None if is_invalid_arr(frs) else frs[ind]
468478
strs = None if is_invalid_arr(strs) else full_3x3_to_voigt_6_stress(strs)
469479
calc = SinglePointDFTCalculator(atoms=atoms, energy=ener['E_KohnSham'],
470480
free_energy=ener['E_KohnSham'],

python/ase-plugin/abacuslite/io/legacyio.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import unittest
44
from io import TextIOWrapper
55
from pathlib import Path
6-
from typing import Dict, List, Tuple
6+
from typing import Dict, List, Tuple, Optional
77

88
import numpy as np
99
from ase.atoms import Atoms
@@ -733,8 +733,10 @@ def is_invalid_arr(arr) -> bool:
733733
return False
734734

735735
# @reader
736-
def read_abacus_out(fileobj, index=slice(None), results_required=True) \
737-
-> Atoms | List[Atoms]:
736+
def read_abacus_out(fileobj,
737+
index=slice(None),
738+
results_required=True,
739+
sort_atoms_with: Optional[List[int]] = None) -> Atoms | List[Atoms]:
738740
'''Reads the ABACUS output files. This function would be called by
739741
the AbacusTemplate.read_results() function. The detailed call stack
740742
is as follows:
@@ -759,6 +761,8 @@ def read_abacus_out(fileobj, index=slice(None), results_required=True) \
759761
Whether the results are required. If True, the results will be
760762
returned. If False, the results will not be returned. This parameter
761763
is not used.
764+
sort_atoms_with : Optional[List[int]]
765+
The index of the atoms to sort. If None, the atoms will not be sorted.
762766
763767
Returns
764768
-------
@@ -817,15 +821,18 @@ def read_abacus_out(fileobj, index=slice(None), results_required=True) \
817821
magmom = [np.zeros(shape=(len(trajectory[0]['elem'])))] * len(trajectory)
818822

819823
# loop over the frame...
820-
images = []
824+
images, ind = [], sort_atoms_with
821825
for frame, estat, mag, frs, strs, ener in zip(
822826
trajectory, elecstate, magmom, forces, stress, energies):
823827
# for each frame, a structure can be defined
824-
atoms = Atoms(symbols=frame['elem'], positions=frame['coords'], cell=frame['cell'])
828+
ind = ind or list(range(len(frame['elem'])))
829+
atoms = Atoms(symbols=np.array(frame['elem'])[ind].tolist(),
830+
positions=frame['coords'][ind],
831+
cell=frame['cell'])
825832
# from result, a calculator can be assembled
826833
# however, sometimes the force and stress is not calculated
827834
# in this case, we set them to None
828-
frs = None if is_invalid_arr(frs) else frs
835+
frs = None if is_invalid_arr(frs) else frs[ind]
829836
strs = None if is_invalid_arr(strs) else full_3x3_to_voigt_6_stress(strs)
830837
calc = SinglePointDFTCalculator(atoms=atoms, energy=ener['E_KohnSham'],
831838
free_energy=ener['E_KohnSham'],

python/ase-plugin/examples/metadynamics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@
7878
ch4f = ''' 6
7979
ABACUS ASE Plugin Metadynamics example structure
8080
C -0.00000000 -0.00000000 -1.50074532
81+
F 0.00000000 -0.00000000 0.88279136
8182
H 0.00000000 -1.01539888 -1.16330635
8283
H -0.87936122 0.50769944 -1.16330635
8384
H 0.87936122 0.50769944 -1.16330635
8485
H -0.00000000 -0.00000000 -2.57055225
85-
F 0.00000000 -0.00000000 0.88279136
8686
'''
8787
with tempfile.NamedTemporaryFile(mode='w', suffix='.xyz') as f:
8888
f.write(ch4f)

python/ase-plugin/examples/neb.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'''this example shows how to use the IDPP method interpolate the
2+
nudged elastic band (NEB) calculations such that the transition
3+
state can be founded more quickly.
4+
5+
In this example, we also use the reaction from the metadynamics.py
6+
, the Walden inversion
7+
'''
8+
9+
import shutil
10+
import tempfile
11+
from pathlib import Path # a more Pythonic alternative to the os.path
12+
here = Path(__file__).parent
13+
# to the directory where the pseudopotential and orbital files are stored
14+
# In your case you change to the appropriate one
15+
pporb = here.parent.parent.parent / 'tests' / 'PP_ORB'
16+
17+
from ase.io import read
18+
from ase.atoms import Atoms
19+
from ase.optimize import BFGS, FIRE
20+
from ase.mep import NEB
21+
from ase.constraints import FixCartesian
22+
from abacuslite import AbacusProfile, Abacus
23+
24+
aprof = AbacusProfile(
25+
command='mpirun -np 16 abacus_2p',
26+
pseudo_dir=pporb,
27+
orbital_dir=pporb,
28+
omp_num_threads=1,
29+
)
30+
PSEUDOPOTENTIALS = {
31+
'C': 'C_ONCV_PBE-1.0.upf',
32+
'H': 'H_ONCV_PBE-1.0.upf',
33+
'F': 'F_ONCV_PBE-1.0.upf',
34+
}
35+
BASISSETS = {
36+
'C': 'C_gga_8au_100Ry_2s2p1d.orb',
37+
'H': 'H_gga_8au_100Ry_2s1p.orb',
38+
'F': 'F_gga_7au_100Ry_2s2p1d.orb',
39+
}
40+
DFTPARAM = {
41+
'calculation': 'scf',
42+
'nspin': 2, # because we are studying a reaction involving radical
43+
'basis_type': 'lcao',
44+
'ks_solver': 'genelpa',
45+
'ecutwfc': 60, # good for example, wrong for production
46+
'symmetry': 1,
47+
'gamma_only': True,
48+
'init_chg': 'auto'
49+
}
50+
51+
def impose_constraint(atoms: Atoms) -> Atoms:
52+
'''imposing the constraint such that the C atom is fixed,
53+
H and F that leaves and arrives are fixed such that can only
54+
move along Z-axis'''
55+
# reset the constraint
56+
atoms.set_constraint(None)
57+
atoms.set_constraint([FixCartesian(a=[0]),
58+
FixCartesian(a=[4, 5], mask=(True, True, False))])
59+
return atoms
60+
61+
def relax(atoms: Atoms) -> Atoms:
62+
'''relax the structure to the minimum energy configuration'''
63+
# we do additional fully-constraint on the No.1 atom, and XY-constraint
64+
# on the last two atoms
65+
print('Relaxation of atoms:', atoms)
66+
with tempfile.TemporaryDirectory() as tmpdir:
67+
calc = Abacus(
68+
profile=aprof,
69+
directory=tmpdir,
70+
pseudopotentials=PSEUDOPOTENTIALS,
71+
basissets=BASISSETS,
72+
inp=DFTPARAM
73+
)
74+
atoms.calc = calc
75+
opt = BFGS(atoms, logfile='-')
76+
opt.run(fmax=0.05)
77+
print('Done')
78+
return atoms
79+
80+
# relax the initial and final states
81+
ch4f = ''' 6
82+
Lattice="15.0 0.0 0.0 0.0 15.0 0.0 0.0 0.0 15.0" Properties=species:S:1:pos:R:3
83+
C -0.00000000 -0.00000000 -1.50074532
84+
H 0.00000000 -1.01539888 -1.16330635
85+
H -0.87936122 0.50769944 -1.16330635
86+
H 0.87936122 0.50769944 -1.16330635
87+
H -0.00000000 -0.00000000 -2.57055225
88+
F 0.00000000 -0.00000000 0.88279136
89+
'''
90+
with tempfile.NamedTemporaryFile(mode='w', suffix='.extxyz') as f:
91+
f.write(ch4f)
92+
f.flush()
93+
ini = relax(impose_constraint(read(f.name)))
94+
95+
ch3fh = ''' 6
96+
Lattice="15.0 0.0 0.0 0.0 15.0 0.0 0.0 0.0 15.0" Properties=species:S:1:pos:R:3
97+
C -0.00000000 -0.00000000 -1.50074532
98+
H 0.00000000 -1.01539888 -1.16330635
99+
H -0.87936122 0.50769944 -1.16330635
100+
H 0.87936122 0.50769944 -1.16330635
101+
H -0.00000000 -0.00000000 -3.57055225
102+
F 0.00000000 -0.00000000 -0.50000000
103+
'''
104+
with tempfile.NamedTemporaryFile(mode='w', suffix='.extxyz') as f:
105+
f.write(ch3fh)
106+
f.flush()
107+
fin = relax(impose_constraint(read(f.name)))
108+
109+
# prepare images
110+
n_replica, images = 5, []
111+
112+
for irep in range(n_replica):
113+
calc = Abacus(
114+
profile=aprof,
115+
directory=here / f'neb-idpp-replica-{irep}',
116+
pseudopotentials=PSEUDOPOTENTIALS,
117+
basissets=BASISSETS,
118+
inp=DFTPARAM
119+
)
120+
if irep == 0:
121+
ini.calc = calc
122+
images.append(impose_constraint(ini))
123+
elif irep == n_replica - 1:
124+
fin.calc = calc
125+
images.append(impose_constraint(fin))
126+
else:
127+
rep = ini.copy() if irep <= n_replica // 2 else fin.copy()
128+
rep.calc = calc
129+
images.append(impose_constraint(rep))
130+
131+
neb = NEB(images)
132+
neb.interpolate('idpp')
133+
134+
# optimize the band
135+
qn = FIRE(neb, trajectory='neb-idpp.traj', logfile='-')
136+
qn.run(fmax=0.05)

python/ase-plugin/examples/relax.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
# AbacusProfile: the interface connecting the Abacus calculator instance
1717
# with the file system and the enviroment
1818
aprof = AbacusProfile(
19-
command='mpirun -np 4 abacus',
19+
command='mpirun -np 8 abacus',
2020
pseudo_dir=pporb,
2121
orbital_dir=pporb,
2222
omp_num_threads=1,
2323
)
2424

2525
# Abacus: the calculator instance
26-
jobdir = here / 'scf'
26+
jobdir = here / 'relax'
2727
abacus = Abacus(
2828
profile=aprof,
2929
directory=str(jobdir),

0 commit comments

Comments
 (0)