8
8
9
9
If you use this module, please cite:
10
10
11
- Gus L. W. Hart and Rodney W. Forcade, "Algorithm for generating derivative
12
- structures," Phys. Rev. B 77 224115 (26 June 2008)
11
+ - Gus L. W. Hart and Rodney W. Forcade, "Algorithm for generating derivative
12
+ structures," Phys. Rev. B 77 224115 (26 June 2008)
13
13
14
- Gus L. W. Hart and Rodney W. Forcade, "Generating derivative structures from
15
- multilattices: Application to hcp alloys," Phys. Rev. B 80 014120 (July 2009)
14
+ - Gus L. W. Hart and Rodney W. Forcade, "Generating derivative structures from
15
+ multilattices: Application to hcp alloys," Phys. Rev. B 80 014120 (July 2009)
16
16
17
- Gus L. W. Hart, Lance J. Nelson, and Rodney W. Forcade, "Generating
18
- derivative structures at a fixed concentration," Comp. Mat. Sci. 59
19
- 101-107 (March 2012)
17
+ - Gus L. W. Hart, Lance J. Nelson, and Rodney W. Forcade, "Generating
18
+ derivative structures at a fixed concentration," Comp. Mat. Sci. 59
19
+ 101-107 (March 2012)
20
20
21
- Wiley S. Morgan, Gus L. W. Hart, Rodney W. Forcade, "Generating derivative
22
- superstructures for systems with high configurational freedom," Comp. Mat.
23
- Sci. 136 144-149 (May 2017)
21
+ - Wiley S. Morgan, Gus L. W. Hart, Rodney W. Forcade, "Generating derivative
22
+ superstructures for systems with high configurational freedom," Comp. Mat.
23
+ Sci. 136 144-149 (May 2017)
24
24
"""
25
25
26
26
from __future__ import annotations
33
33
import subprocess
34
34
from glob import glob
35
35
from shutil import which
36
- from threading import Timer
36
+ from typing import TYPE_CHECKING
37
37
38
38
import numpy as np
39
39
from monty .dev import requires
44
44
from pymatgen .io .vasp .inputs import Poscar
45
45
from pymatgen .symmetry .analyzer import SpacegroupAnalyzer
46
46
47
+ if TYPE_CHECKING :
48
+ from typing import ClassVar
49
+
47
50
logger = logging .getLogger (__name__ )
48
51
49
- # Favor the use of the newer "enum.x" by Gus Hart instead of the older
50
- # "multienum.x"
51
- enum_cmd = which ("enum.x" ) or which ("multienum.x" )
52
- # prefer makestr.x at present
53
- makestr_cmd = which ("makestr.x" ) or which ("makeStr.x" ) or which ("makeStr.py" )
52
+ # Favor the use of the newer "enum.x" by Gus Hart over "multienum.x"
53
+ ENUM_CMD = which ("enum.x" ) or which ("multienum.x" )
54
+ # Prefer makestr.x at present
55
+ MAKESTR_CMD = which ("makestr.x" ) or which ("makeStr.x" ) or which ("makeStr.py" )
54
56
55
57
56
58
@requires (
57
- enum_cmd and makestr_cmd ,
59
+ ENUM_CMD and MAKESTR_CMD ,
58
60
"EnumlibAdaptor requires the executables 'enum.x' or 'multienum.x' "
59
61
"and 'makestr.x' or 'makeStr.py' to be in the path. Please download the "
60
62
"library at https://github.com/msg-byu/enumlib and follow the instructions "
@@ -64,26 +66,26 @@ class EnumlibAdaptor:
64
66
"""An adaptor for enumlib.
65
67
66
68
Attributes:
67
- structures (list): all enumerated structures.
69
+ structures (list[Structure] ): all enumerated structures.
68
70
"""
69
71
70
- amount_tol = 1e-5
72
+ amount_tol : ClassVar [ float ] = 1e-5
71
73
72
74
def __init__ (
73
75
self ,
74
- structure ,
75
- min_cell_size = 1 ,
76
- max_cell_size = 1 ,
77
- symm_prec = 0.1 ,
78
- enum_precision_parameter = 0.001 ,
79
- refine_structure = False ,
80
- check_ordered_symmetry = True ,
81
- timeout = None ,
82
- ):
76
+ structure : Structure ,
77
+ min_cell_size : int = 1 ,
78
+ max_cell_size : int = 1 ,
79
+ symm_prec : float = 0.1 ,
80
+ enum_precision_parameter : float = 0.001 ,
81
+ refine_structure : bool = False ,
82
+ check_ordered_symmetry : bool = True ,
83
+ timeout : float | None = None ,
84
+ ) -> None :
83
85
"""Initialize the adapter with a structure and some parameters.
84
86
85
87
Args:
86
- structure: An input structure.
88
+ structure (Structure) : An input structure.
87
89
min_cell_size (int): The minimum cell size wanted. Defaults to 1.
88
90
max_cell_size (int): The maximum cell size wanted. Defaults to 1.
89
91
symm_prec (float): Symmetry precision. Defaults to 0.1.
@@ -117,61 +119,65 @@ def __init__(
117
119
self .structure = finder .get_refined_structure ()
118
120
else :
119
121
self .structure = structure
122
+
120
123
self .min_cell_size = min_cell_size
121
124
self .max_cell_size = max_cell_size
122
125
self .symm_prec = symm_prec
123
126
self .enum_precision_parameter = enum_precision_parameter
124
127
self .check_ordered_symmetry = check_ordered_symmetry
125
128
self .timeout = timeout
126
129
127
- def run (self ):
130
+ def run (self ) -> None :
128
131
"""Run the enumeration."""
129
- # Create a temporary directory for working.
132
+ # Work in a temporary directory
130
133
with ScratchDir ("." ) as tmp_dir :
131
134
logger .debug (f"Temp dir : { tmp_dir } " )
132
135
# Generate input files
133
136
self ._gen_input_file ()
137
+
134
138
# Perform the actual enumeration
135
139
num_structs = self ._run_multienum ()
140
+
136
141
# Read in the enumeration output as structures.
137
142
if num_structs > 0 :
138
143
self .structures = self ._get_structures (num_structs )
139
144
else :
140
145
raise EnumError ("Unable to enumerate structure." )
141
146
142
- def _gen_input_file (self ):
147
+ def _gen_input_file (self ) -> None :
143
148
"""Generate the necessary struct_enum.in file for enumlib. See enumlib
144
149
documentation for details.
145
150
"""
146
151
coord_format = "{:.6f} {:.6f} {:.6f}"
147
- # Using symmetry finder, get the symmetrically distinct sites.
152
+ # Use symmetry finder to get the symmetrically distinct sites
148
153
fitter = SpacegroupAnalyzer (self .structure , self .symm_prec )
149
154
symmetrized_structure = fitter .get_symmetrized_structure ()
150
155
logger .debug (
151
156
f"Spacegroup { fitter .get_space_group_symbol ()} ({ fitter .get_space_group_number ()} ) "
152
157
f"with { len (symmetrized_structure .equivalent_sites )} distinct sites"
153
158
)
154
- """Enumlib doesn"t work when the number of species get too large. To
155
- simplify matters, we generate the input file only with disordered sites
156
- and exclude the ordered sites from the enumeration. The fact that
157
- different disordered sites with the exact same species may belong to
158
- different equivalent sites is dealt with by having determined the
159
- spacegroup earlier and labelling the species differently.
160
- """
161
- # index_species and index_amounts store mappings between the indices
159
+
160
+ # Enumlib doesn"t work when the number of species get too large. To
161
+ # simplify matters, we generate the input file only with disordered sites
162
+ # and exclude the ordered sites from the enumeration. The fact that
163
+ # different disordered sites with the exact same species may belong to
164
+ # different equivalent sites is dealt with by having determined the
165
+ # spacegroup earlier and labelling the species differently.
166
+
167
+ # `index_species` and `index_amounts` store mappings between the indices
162
168
# used in the enum input file, and the actual species and amounts.
163
169
index_species = []
164
170
index_amounts = []
165
171
166
- # Stores the ordered sites, which are not enumerated.
172
+ # Store the ordered sites, which are not enumerated.
167
173
ordered_sites = []
168
174
disordered_sites = []
169
175
coord_str = []
170
176
for sites in symmetrized_structure .equivalent_sites :
171
177
if sites [0 ].is_ordered :
172
178
ordered_sites .append (sites )
173
179
else :
174
- sp_label = []
180
+ _sp_label : list [ int ] = []
175
181
species = dict (sites [0 ].species .items ())
176
182
if sum (species .values ()) < 1 - EnumlibAdaptor .amount_tol :
177
183
# Let us first make add a dummy element for every single
@@ -180,42 +186,41 @@ def _gen_input_file(self):
180
186
for sp , amt in species .items ():
181
187
if sp not in index_species :
182
188
index_species .append (sp )
183
- sp_label .append (len (index_species ) - 1 )
189
+ _sp_label .append (len (index_species ) - 1 )
184
190
index_amounts .append (amt * len (sites ))
185
191
else :
186
192
ind = index_species .index (sp )
187
- sp_label .append (ind )
193
+ _sp_label .append (ind )
188
194
index_amounts [ind ] += amt * len (sites )
189
- sp_label = "/" .join (f"{ i } " for i in sorted (sp_label ))
195
+ sp_label : str = "/" .join (f"{ i } " for i in sorted (_sp_label ))
190
196
for site in sites :
191
197
coord_str .append (f"{ coord_format .format (* site .coords )} { sp_label } " )
192
198
disordered_sites .append (sites )
193
199
194
- def get_sg_info (ss ):
200
+ def get_sg_number (ss ) -> int :
195
201
finder = SpacegroupAnalyzer (Structure .from_sites (ss ), self .symm_prec )
196
202
return finder .get_space_group_number ()
197
203
198
- target_sg_num = get_sg_info (list (symmetrized_structure ))
204
+ target_sg_num = get_sg_number (list (symmetrized_structure ))
199
205
curr_sites = list (itertools .chain .from_iterable (disordered_sites ))
200
- sg_num = get_sg_info (curr_sites )
206
+ sg_num = get_sg_number (curr_sites )
201
207
ordered_sites = sorted (ordered_sites , key = len )
202
208
logger .debug (f"Disordered sites has sg # { sg_num } " )
203
209
self .ordered_sites = []
204
210
205
- # progressively add ordered sites to our disordered sites
211
+ # Progressively add ordered sites to our disordered sites
206
212
# until we match the symmetry of our input structure
207
213
if self .check_ordered_symmetry :
208
214
while sg_num != target_sg_num and len (ordered_sites ) > 0 :
209
215
sites = ordered_sites .pop (0 )
210
216
temp_sites = list (curr_sites ) + sites
211
- new_sg_num = get_sg_info (temp_sites )
217
+ new_sg_num = get_sg_number (temp_sites )
212
218
if sg_num != new_sg_num :
213
219
logger .debug (f"Adding { sites [0 ].specie } in enum. New sg # { new_sg_num } " )
214
220
index_species .append (sites [0 ].specie )
215
221
index_amounts .append (len (sites ))
216
- sp_label = len (index_species ) - 1
217
222
for site in sites :
218
- coord_str .append (f"{ coord_format .format (* site .coords )} { sp_label } " )
223
+ coord_str .append (f"{ coord_format .format (* site .coords )} { len ( index_species ) - 1 } " )
219
224
disordered_sites .append (sites )
220
225
curr_sites = temp_sites
221
226
sg_num = new_sg_num
@@ -274,29 +279,29 @@ def get_sg_info(ss):
274
279
min_conc = math .floor (conc * base )
275
280
output .append (f"{ min_conc - 1 } { min_conc + 1 } { base } " )
276
281
output .append ("" )
282
+
277
283
logger .debug ("Generated input file:\n " + "\n " .join (output ))
278
284
with open ("struct_enum.in" , mode = "w" , encoding = "utf-8" ) as file :
279
285
file .write ("\n " .join (output ))
280
286
281
- def _run_multienum (self ):
282
- with subprocess .Popen ([enum_cmd ], stdout = subprocess .PIPE , stdin = subprocess .PIPE , close_fds = True ) as process :
283
- if self .timeout :
284
- timed_out = False
285
- timer = Timer (self .timeout * 60 , lambda p : p .kill (), [process ])
287
+ def _run_multienum (self ) -> int :
288
+ """Run enumlib to get multiple structure.
286
289
287
- try :
288
- timer .start ()
289
- output = process .communicate ()[0 ].decode ("utf-8" )
290
- finally :
291
- if not timer .is_alive ():
292
- timed_out = True
293
- timer .cancel ()
290
+ Returns:
291
+ int: number of structures.
292
+ """
293
+ if ENUM_CMD is None :
294
+ raise RuntimeError ("enumlib is not available" )
294
295
295
- if timed_out :
296
- raise TimeoutError ( "Enumeration took too long" )
296
+ with subprocess . Popen ([ ENUM_CMD ], stdout = subprocess . PIPE , stdin = subprocess . PIPE , close_fds = True ) as process :
297
+ timeout = self . timeout * 60 if self . timeout is not None else None
297
298
298
- else :
299
- output = process .communicate ()[0 ].decode ("utf-8" )
299
+ try :
300
+ output = process .communicate (timeout = timeout )[0 ].decode ("utf-8" )
301
+ except subprocess .TimeoutExpired as exc :
302
+ process .kill ()
303
+ process .wait ()
304
+ raise TimeoutError (f"Enumeration took more than timeout { self .timeout } minutes" ) from exc
300
305
301
306
count = 0
302
307
start_count = False
@@ -308,37 +313,41 @@ def _run_multienum(self):
308
313
logger .debug (f"Enumeration resulted in { count } structures" )
309
314
return count
310
315
311
- def _get_structures (self , num_structs ):
312
- structs = []
316
+ def _get_structures (self , num_structs : int ) -> list [Structure ]:
317
+ if MAKESTR_CMD is None :
318
+ raise RuntimeError ("makestr.x is not available" )
319
+
320
+ structs : list [Structure ] = []
313
321
314
- if ".py" in makestr_cmd :
315
- options = [ "-input" , "struct_enum.out" , str (1 ), str (num_structs )]
322
+ if ".py" in MAKESTR_CMD :
323
+ options : tuple [ str , ...] = ( "-input" , "struct_enum.out" , str (1 ), str (num_structs ))
316
324
else :
317
- options = [ "struct_enum.out" , str (0 ), str (num_structs - 1 )]
325
+ options = ( "struct_enum.out" , str (0 ), str (num_structs - 1 ))
318
326
319
327
with subprocess .Popen (
320
- [makestr_cmd , * options ],
328
+ [MAKESTR_CMD , * options ],
321
329
stdout = subprocess .PIPE ,
322
330
stdin = subprocess .PIPE ,
323
331
close_fds = True ,
324
332
) as rs :
325
333
_stdout , stderr = rs .communicate ()
334
+
326
335
if stderr :
327
336
logger .warning (stderr .decode ())
328
337
329
- # sites retrieved from enumlib will lack site properties
338
+ # Sites retrieved from enumlib will lack site properties
330
339
# to ensure consistency, we keep track of what site properties
331
340
# are missing and set them to None
332
341
# TODO: improve this by mapping ordered structure to original
333
342
# disordered structure, and retrieving correct site properties
334
- disordered_site_properties = {}
343
+ disordered_site_properties : dict = {}
335
344
336
345
if len (self .ordered_sites ) > 0 :
337
346
original_latt = self .ordered_sites [0 ].lattice
338
347
# Need to strip sites of site_properties, which would otherwise
339
348
# result in an index error. Hence Structure is reconstructed in
340
349
# the next step.
341
- site_properties = {}
350
+ site_properties : dict = {}
342
351
for site in self .ordered_sites :
343
352
for k , v in site .properties .items ():
344
353
disordered_site_properties [k ] = None
@@ -357,8 +366,8 @@ def _get_structures(self, num_structs):
357
366
ordered_structure = inv_org_latt = None
358
367
359
368
for file in glob ("vasp.*" ):
360
- with open (file , encoding = "utf-8" ) as file :
361
- data = file .read ()
369
+ with open (file , encoding = "utf-8" ) as _file :
370
+ data = _file .read ()
362
371
data = re .sub (r"scale factor" , "1" , data )
363
372
data = re .sub (r"(\d+)-(\d+)" , r"\1 -\2" , data )
364
373
poscar = Poscar .from_str (data , self .index_species )
0 commit comments