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

182 statements  

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

1import os 

2import random 

3from io import StringIO 

4 

5 

6from xraydb import atomic_symbol, atomic_number, xray_edge 

7from larch.utils import fix_varname, strict_ascii, gformat 

8 

9from .amcsd_utils import PMG_CIF_OPTS, CifParser, Molecule, SpacegroupAnalyzer 

10 

11def get_atom_map(structure): 

12 """generalization of pymatgen atom map 

13 Returns: 

14 dict of ipots 

15 """ 

16 unique_pot_atoms = [] 

17 all_sites = [] 

18 for site in structure: 

19 for elem in site.species.elements: 

20 if elem.symbol not in unique_pot_atoms: 

21 unique_pot_atoms.append(elem.symbol) 

22 

23 atom_map = {} 

24 for i, atom in enumerate(unique_pot_atoms): 

25 atom_map[atom] = i + 1 

26 return atom_map 

27 

28 

29def read_cif_structure(ciftext): 

30 """read CIF text, return CIF Structure 

31 

32 Arguments 

33 --------- 

34 ciftext (string): text of CIF file or name of the CIF file. 

35 

36 Returns 

37 ------- 

38 pymatgen Structure object 

39 """ 

40 if CifParser is None: 

41 raise ValueError("CifParser from pymatgen not available. Try 'pip install pymatgen'.") 

42 try: 

43 cifstructs = CifParser(StringIO(ciftext), **PMG_CIF_OPTS) 

44 parse_ok = True 

45 file_found = True 

46 except: 

47 parse_ok = False 

48 file_found = False 

49 if os.path.exists(ciftext): 

50 file_found = True 

51 try: 

52 cifstructs = CifParser(ciftext, **PMG_CIF_OPTS) 

53 parse_ok = True 

54 except: 

55 parse_ok = False 

56 

57 try: 

58 cstruct = cifstructs.get_structures()[0] 

59 except: 

60 raise ValueError('could not get structure from text of CIF file') 

61 

62 if not parse_ok: 

63 if not file_found: 

64 raise FileNotFoundError(f'file {ciftext:s} not found') 

65 else: 

66 raise ValueError('invalid text of CIF file') 

67 return cstruct 

68 

69def cif_sites(ciftext, absorber=None): 

70 "return list of sites for the structure" 

71 cstruct = read_cif_structure(ciftext) 

72 out = cstruct.sites 

73 if absorber is not None: 

74 abname = absorber.lower() 

75 out = [] 

76 for site in cstruct.sites: 

77 species = site.species_string.lower() 

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

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

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

81 if sname.strip() == abname: 

82 out.append(site) 

83 elif species == abname: 

84 out.append(site) 

85 if len(out) == 0: 

86 out = cstruct.sites[0] 

87 return out 

88 

89 

90def cif2feffinp(ciftext, absorber, edge=None, cluster_size=8.0, absorber_site=1, 

91 site_index=None, extra_titles=None, with_h=False, version8=True): 

92 """convert CIF text to Feff8 or Feff6l input file 

93 

94 Arguments 

95 --------- 

96 ciftext (string): text of CIF file or name of the CIF file. 

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

98 (see Note 1) 

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

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

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

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

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

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

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

106 Returns 

107 ------- 

108 text of Feff input file 

109 

110 Notes 

111 ----- 

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

113 must be an element in the CIF structure. 

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

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

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

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

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

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

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

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

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

123 site_index (starting at 1!) 

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

125 

126 """ 

127 try: 

128 cstruct = read_cif_structure(ciftext) 

129 except ValueError: 

130 return '# could not read CIF file' 

131 

132 sgroup = SpacegroupAnalyzer(cstruct).get_symmetry_dataset() 

133 space_group = sgroup["international"] 

134 

135 if isinstance(absorber, int): 

136 absorber = atomic_symbol(absorber_z) 

137 absorber_z = atomic_number(absorber) 

138 

139 if edge is None: 

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

141 

142 edge_energy = xray_edge(absorber, edge).energy 

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

144 

145 unique_pot_atoms = [] 

146 for site in cstruct: 

147 for elem in site.species.elements: 

148 if elem.symbol not in unique_pot_atoms: 

149 unique_pot_atoms.append(elem.symbol) 

150 

151 atoms_map = {} 

152 for i, atom in enumerate(unique_pot_atoms): 

153 atoms_map[atom] = i + 1 

154 

155 if absorber not in atoms_map: 

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

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

158 

159 

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

161 site_tags = {} 

162 absorber_count = 0 

163 for sindex, site in enumerate(cstruct.sites): 

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

165 if len(site_species) > 1: 

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

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

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

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

170 else: 

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

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

173 if absorber in site_species: 

174 absorber_count += 1 

175 if absorber_count == absorber_site: 

176 absorber_index = sindex 

177 

178 if site_index is not None: 

179 absorber_index = site_index - 1 

180 

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

182 

183 center = cstruct[absorber_index].coords 

184 sphere = cstruct.get_neighbors(cstruct[absorber_index], cluster_size) 

185 symbols = [absorber] 

186 coords = [[0, 0, 0]] 

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

188 

189 for i, site_dist in enumerate(sphere): 

190 s_index = site_dist[0].index 

191 

192 site_symbol = site_atoms[s_index].pop() 

193 tags.append(site_tags[s_index]) 

194 symbols.append(site_symbol) 

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

196 cluster = Molecule(symbols, coords) 

197 

198 out_text = ['*** feff input generated by xraylarch cif2feff using pymatgen ***'] 

199 

200 if extra_titles is not None: 

201 for etitle in extra_titles[:]: 

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

203 etitle = 'TITLE ' + etitle 

204 out_text.append(etitle) 

205 

206 out_text.append(f'TITLE Formula: {cstruct.composition.reduced_formula:s}') 

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

208 out_text.append(f'TITLE # sites: {cstruct.num_sites}') 

209 

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

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

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

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

214 for i, site in enumerate(cstruct): 

215 fc = site.frac_coords 

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

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

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

219 

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

221 

222 if version8: 

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

224 out_text.append('S02 1.0') 

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

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

227 out_text.append('EXAFS 20.0') 

228 out_text.append('NLEG 6') 

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

230 out_text.append('*SCF 5.0') 

231 

232 else: 

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

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

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

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

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

238 

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

240 '* POLARIZATION 0 0 0', '', 

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

242 

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

244 # (maybe fractional occupation) is not included 

245 

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

247 ipot_map = {} 

248 next_ipot = 0 

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

250 sym = site.species_string 

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

252 continue 

253 if sym in ipot_map: 

254 ipot = ipot_map[sym] 

255 else: 

256 next_ipot += 1 

257 ipot_map[sym] = ipot = next_ipot 

258 

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

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

261 

262 

263 ipot, z = 0, absorber_z 

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

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

266 z = atomic_number(sym) 

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

268 

269 out_text.append('') 

270 out_text.append('ATOMS') 

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

272 

273 acount = 0 

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

275 acount += 1 

276 if acount > 500: 

277 break 

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

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

280 

281 out_text.append('') 

282 out_text.append('* END') 

283 out_text.append('') 

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