Coverage for /Users/Newville/Codes/xraylarch/larch/xrd/structure2feff.py: 5%

194 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-09 10:08 -0600

1import os 

2import random 

3 

4from .amcsd_utils import (SpacegroupAnalyzer, Molecule, IMolecule, IStructure) 

5 

6from xraydb import atomic_symbol, atomic_number, xray_edge 

7from larch.utils.strutils import fix_varname, strict_ascii 

8 

9 

10def get_atom_map(structure): 

11 """generalization of pymatgen atom map 

12 Returns: 

13 dict of ipots 

14 """ 

15 unique_pot_atoms = [] 

16 all_sites = [] 

17 for site in structure: 

18 for elem in site.species.elements: 

19 if elem.symbol not in unique_pot_atoms: 

20 unique_pot_atoms.append(elem.symbol) 

21 

22 atom_map = {} 

23 for i, atom in enumerate(unique_pot_atoms): 

24 atom_map[atom] = i + 1 

25 return atom_map 

26 

27 

28def read_structure(structure_text, fmt="cif"): 

29 """read structure from text 

30 

31 Arguments 

32 --------- 

33 structure_text (string): text of structure file 

34 fmt (string): format of structure file (cif, poscar, etc) 

35 

36 Returns 

37 ------- 

38 pymatgen Structure object or Molecule object 

39 """ 

40 if Molecule is None: 

41 raise ImportError("pymatgen required. Try 'pip install pymatgen'.") 

42 try: 

43 if fmt.lower() in ('cif', 'poscar', 'contcar', 'chgcar', 'locpot', 'cssr', 'vasprun.xml'): 

44 struct = IStructure.from_str(structure_text, fmt, merge_tol=5.e-4) 

45 else: 

46 struct = IMolecule.from_str(structure_text, fmt) 

47 parse_ok = True 

48 file_found = True 

49 

50 except: 

51 parse_ok = False 

52 file_found = False 

53 if os.path.exists(structure_text): 

54 file_found = True 

55 fmt = os.path.splitext(structure_text)[-1].lower() 

56 try: 

57 if fmt.lower() in ('cif', 'poscar', 'contcar', 'chgcar', 'locpot', 'cssr', 'vasprun.xml'): 

58 struct = IStructure.from_file(structure_text, merge_tol=5.e-4) 

59 else: 

60 struct = IMolecule.from_file(structure_text) 

61 parse_ok = True 

62 except: 

63 parse_ok = False 

64 

65 if not parse_ok: 

66 if not file_found: 

67 raise FileNotFoundError(f'file {structure_text:s} not found') 

68 else: 

69 raise ValueError('invalid text of structure file') 

70 return struct 

71 

72def structure_sites(structure_text, absorber=None, fmt='cif'): 

73 "return list of sites for the structure" 

74 struct = read_structure(structure_text, fmt=fmt) 

75 out = struct.sites 

76 if absorber is not None: 

77 abname = absorber.lower() 

78 out = [] 

79 for site in struct.sites: 

80 species = site.species_string.lower() 

81 if ',' in species and ':' in species: # multi-occupancy site 

82 for siteocc in species.split(','): 

83 sname, occ = siteocc.split(':') 

84 if sname.strip() == abname: 

85 out.append(site) 

86 elif species == abname: 

87 out.append(site) 

88 if len(out) == 0: 

89 out = struct.sites[0] 

90 return out 

91 

92def parse_structure(structure_text, fmt='cif', fname="default.filename"): 

93 try: 

94 struct = read_structure(structure_text, fmt=fmt) 

95 except ValueError: 

96 return '# could not read structure file' 

97 

98 return {'formula': struct.composition.reduced_formula, 'sites': struct.sites, 'structure_text': structure_text, 'fmt': fmt, 'fname': fname} 

99 

100 

101def structure2feffinp(structure_text, absorber, edge=None, cluster_size=8.0, absorber_site=1, 

102 site_index=None, extra_titles=None, with_h=False, version8=True, fmt='cif'): 

103 """convert structure text to Feff8 or Feff6l input file 

104 

105 Arguments 

106 --------- 

107 structure_text (string): text of CIF file or name of the CIF file. 

108 absorber (string or int): atomic symbol or atomic number of absorbing element 

109 (see Note 1) 

110 edge (string or None): edge for calculation (see Note 2) [None] 

111 cluster_size (float): size of cluster, in Angstroms [8.0] 

112 absorber_site (int): index of site for absorber (see Note 3) [1] 

113 site_index (int or None): index of site for absorber (see Note 4) [None] 

114 extra_titles (list of str or None): extra title lines to include [None] 

115 with_h (bool): whether to include H atoms [False] 

116 version8 (bool): whether to write Feff8l input (see Note 5)[True] 

117 fmt (string): format of structure file (cif, poscar, etc) [cif] 

118 Returns 

119 ------- 

120 text of Feff input file 

121 

122 Notes 

123 ----- 

124 1. absorber is the atomic symbol or number of the absorbing element, and 

125 must be an element in the CIF structure. 

126 2. If edge is a string, it must be one of 'K', 'L', 'M', or 'N' edges (note 

127 Feff6 supports only 'K', 'L3', 'L2', and 'L1' edges). If edge is None, 

128 it will be assigned to be 'K' for absorbers with Z < 58 (Ce, with an 

129 edge energy < 40 keV), and 'L3' for absorbers with Z >= 58. 

130 3. for structures with multiple sites for the absorbing atom, the site 

131 can be selected by the order in which they are listed in the sites 

132 list. This depends on the details of the CIF structure, which can be 

133 found with `cif_sites(ciftext)`, starting counting by 1. 

134 4. to explicitly state the index of the site in the sites list, use 

135 site_index (starting at 1!) 

136 5. if version8 is False, outputs will be written for Feff6l 

137 

138 """ 

139 try: 

140 struct = read_structure(structure_text, fmt=fmt) 

141 except ValueError: 

142 return '# could not read structure file' 

143 

144 is_molecule = False 

145 

146 if isinstance(struct, IStructure): 

147 sgroup = SpacegroupAnalyzer(struct).get_symmetry_dataset() 

148 space_group = sgroup["international"] 

149 else: 

150 space_group = 'Molecule' 

151 is_molecule = True 

152 

153 

154 if isinstance(absorber, int): 

155 absorber = atomic_symbol(absorber_z) 

156 absorber_z = atomic_number(absorber) 

157 

158 if edge is None: 

159 edge = 'K' if absorber_z < 58 else 'L3' 

160 

161 edge_energy = xray_edge(absorber, edge).energy 

162 edge_comment = f'{absorber:s} {edge:s} edge, around {edge_energy:.0f} eV' 

163 

164 unique_pot_atoms = [] 

165 for site in struct: 

166 for elem in site.species.elements: 

167 if elem.symbol not in unique_pot_atoms: 

168 unique_pot_atoms.append(elem.symbol) 

169 

170 atoms_map = {} 

171 for i, atom in enumerate(unique_pot_atoms): 

172 atoms_map[atom] = i + 1 

173 

174 if absorber not in atoms_map: 

175 atlist = ', '.join(atoms_map.keys()) 

176 raise ValueError(f'atomic symbol {absorber:s} not listed in structure data: ({atlist})') 

177 

178 

179 site_atoms = {} # map xtal site with list of atoms occupying that site 

180 site_tags = {} 

181 absorber_count = 0 

182 for sindex, site in enumerate(struct.sites): 

183 site_species = [e.symbol for e in site.species] 

184 if len(site_species) > 1: 

185 s_els = [s.symbol for s in site.species.keys()] 

186 s_wts = [s for s in site.species.values()] 

187 site_atoms[sindex] = random.choices(s_els, weights=s_wts, k=1000) 

188 site_tags[sindex] = f'({site.species_string:s})_{1+sindex:d}' 

189 else: 

190 site_atoms[sindex] = [site_species[0]] * 1000 

191 site_tags[sindex] = f'{site.species_string:s}_{1+sindex:d}' 

192 if absorber in site_species: 

193 absorber_count += 1 

194 if absorber_count == absorber_site: 

195 absorber_index = sindex 

196 

197 if site_index is not None: 

198 absorber_index = site_index - 1 

199 

200 # print("Got sites ", len(cstruct.sites), len(site_atoms), len(site_tags)) 

201 

202 center = struct[absorber_index].coords 

203 sphere = struct.get_neighbors(struct[absorber_index], cluster_size) 

204 symbols = [absorber] 

205 coords = [[0, 0, 0]] 

206 tags = [f'{absorber:s}_{1+absorber_index:d}'] 

207 

208 for i, site_dist in enumerate(sphere): 

209 s_index = site_dist[0].index 

210 

211 site_symbol = site_atoms[s_index].pop() 

212 tags.append(site_tags[s_index]) 

213 symbols.append(site_symbol) 

214 coords.append(site_dist[0].coords - center) 

215 cluster = Molecule(symbols, coords) 

216 

217 out_text = ['*** feff input generated by xraylarch structure2feff using pymatgen ***'] 

218 

219 if extra_titles is not None: 

220 for etitle in extra_titles[:]: 

221 if not etitle.startswith('TITLE '): 

222 etitle = 'TITLE ' + etitle 

223 out_text.append(etitle) 

224 

225 out_text.append(f'TITLE Formula: {struct.composition.reduced_formula:s}') 

226 out_text.append(f'TITLE SpaceGroup: {space_group:s}') 

227 out_text.append(f'TITLE # sites: {struct.num_sites}') 

228 

229 out_text.append('* crystallographics sites: note that these sites may not be unique!') 

230 out_text.append(f'* using absorber at site {1+absorber_index:d} in the list below') 

231 out_text.append(f'* selected as absorber="{absorber:s}", absorber_site={absorber_site:d}') 

232 out_text.append('* index X Y Z species') 

233 

234 for i, site in enumerate(struct): 

235 # The method of obtaining the cooridanates depends on whether the structure is a molecule or not 

236 if is_molecule: 

237 fc = site.coords 

238 else: 

239 fc = site.frac_coords 

240 species_string = fix_varname(site.species_string.strip()) 

241 marker = ' <- absorber' if (i == absorber_index) else '' 

242 out_text.append(f'* {i+1:3d} {fc[0]:.6f} {fc[1]:.6f} {fc[2]:.6f} {species_string:s} {marker:s}') 

243 

244 out_text.extend(['* ', '', '']) 

245 

246 if version8: 

247 out_text.append(f'EDGE {edge:s}') 

248 out_text.append('S02 1.0') 

249 out_text.append('CONTROL 1 1 1 1 1 1') 

250 out_text.append('PRINT 1 0 0 0 0 3') 

251 out_text.append('EXAFS 20.0') 

252 out_text.append('NLEG 6') 

253 out_text.append(f'RPATH {cluster_size:.2f}') 

254 out_text.append('*SCF 5.0') 

255 

256 else: 

257 edge_index = {'K': 1, 'L1': 2, 'L2': 3, 'L3': 4}[edge] 

258 out_text.append(f'HOLE {edge_index:d} 1.0 * {edge_comment:s} (2nd number is S02)') 

259 out_text.append('CONTROL 1 1 1 0 * phase, paths, feff, chi') 

260 out_text.append('PRINT 1 0 0 0') 

261 out_text.append(f'RMAX {cluster_size:.2f}') 

262 

263 out_text.extend(['', 'EXCHANGE 0', '', 

264 '* POLARIZATION 0 0 0', '', 

265 'POTENTIALS', '* IPOT Z Tag']) 

266 

267 # loop to find atoms actually in cluster, in case some atom 

268 # (maybe fractional occupation) is not included 

269 

270 at_lines = [(0, cluster[0].x, cluster[0].y, cluster[0].z, 0, absorber, tags[0])] 

271 ipot_map = {} 

272 next_ipot = 0 

273 for i, site in enumerate(cluster[1:]): 

274 sym = site.species_string 

275 if sym == 'H' and not with_h: 

276 continue 

277 if sym in ipot_map: 

278 ipot = ipot_map[sym] 

279 else: 

280 next_ipot += 1 

281 ipot_map[sym] = ipot = next_ipot 

282 

283 dist = cluster.get_distance(0, i+1) 

284 at_lines.append((dist, site.x, site.y, site.z, ipot, sym, tags[i+1])) 

285 

286 

287 ipot, z = 0, absorber_z 

288 out_text.append(f' {ipot:4d} {z:4d} {absorber:s}') 

289 for sym, ipot in ipot_map.items(): 

290 z = atomic_number(sym) 

291 out_text.append(f' {ipot:4d} {z:4d} {sym:s}') 

292 

293 out_text.append('') 

294 out_text.append('ATOMS') 

295 out_text.append(f'* x y z ipot tag distance site_info') 

296 

297 acount = 0 

298 for dist, x, y, z, ipot, sym, tag in sorted(at_lines, key=lambda x: x[0]): 

299 acount += 1 

300 if acount > 500: 

301 break 

302 sym = (sym + ' ')[:2] 

303 out_text.append(f' {x: .5f} {y: .5f} {z: .5f} {ipot:4d} {sym:s} {dist:.5f} * {tag:s}') 

304 

305 out_text.append('') 

306 out_text.append('* END') 

307 out_text.append('') 

308 return strict_ascii('\n'.join(out_text))