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
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-09 10:08 -0600
1import os
2import random
4from .amcsd_utils import (SpacegroupAnalyzer, Molecule, IMolecule, IStructure)
6from xraydb import atomic_symbol, atomic_number, xray_edge
7from larch.utils.strutils import fix_varname, strict_ascii
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)
22 atom_map = {}
23 for i, atom in enumerate(unique_pot_atoms):
24 atom_map[atom] = i + 1
25 return atom_map
28def read_structure(structure_text, fmt="cif"):
29 """read structure from text
31 Arguments
32 ---------
33 structure_text (string): text of structure file
34 fmt (string): format of structure file (cif, poscar, etc)
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
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
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
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
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'
98 return {'formula': struct.composition.reduced_formula, 'sites': struct.sites, 'structure_text': structure_text, 'fmt': fmt, 'fname': fname}
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
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
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
138 """
139 try:
140 struct = read_structure(structure_text, fmt=fmt)
141 except ValueError:
142 return '# could not read structure file'
144 is_molecule = False
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
154 if isinstance(absorber, int):
155 absorber = atomic_symbol(absorber_z)
156 absorber_z = atomic_number(absorber)
158 if edge is None:
159 edge = 'K' if absorber_z < 58 else 'L3'
161 edge_energy = xray_edge(absorber, edge).energy
162 edge_comment = f'{absorber:s} {edge:s} edge, around {edge_energy:.0f} eV'
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)
170 atoms_map = {}
171 for i, atom in enumerate(unique_pot_atoms):
172 atoms_map[atom] = i + 1
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})')
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
197 if site_index is not None:
198 absorber_index = site_index - 1
200 # print("Got sites ", len(cstruct.sites), len(site_atoms), len(site_tags))
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}']
208 for i, site_dist in enumerate(sphere):
209 s_index = site_dist[0].index
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)
217 out_text = ['*** feff input generated by xraylarch structure2feff using pymatgen ***']
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)
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}')
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')
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}')
244 out_text.extend(['* ', '', ''])
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')
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}')
263 out_text.extend(['', 'EXCHANGE 0', '',
264 '* POLARIZATION 0 0 0', '',
265 'POTENTIALS', '* IPOT Z Tag'])
267 # loop to find atoms actually in cluster, in case some atom
268 # (maybe fractional occupation) is not included
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
283 dist = cluster.get_distance(0, i+1)
284 at_lines.append((dist, site.x, site.y, site.z, ipot, sym, tags[i+1]))
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}')
293 out_text.append('')
294 out_text.append('ATOMS')
295 out_text.append(f'* x y z ipot tag distance site_info')
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}')
305 out_text.append('')
306 out_text.append('* END')
307 out_text.append('')
308 return strict_ascii('\n'.join(out_text))