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
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-09 10:08 -0600
1import os
2import random
3from io import StringIO
6from xraydb import atomic_symbol, atomic_number, xray_edge
7from larch.utils import fix_varname, strict_ascii, gformat
9from .amcsd_utils import PMG_CIF_OPTS, CifParser, Molecule, SpacegroupAnalyzer
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)
23 atom_map = {}
24 for i, atom in enumerate(unique_pot_atoms):
25 atom_map[atom] = i + 1
26 return atom_map
29def read_cif_structure(ciftext):
30 """read CIF text, return CIF Structure
32 Arguments
33 ---------
34 ciftext (string): text of CIF file or name of the CIF file.
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
57 try:
58 cstruct = cifstructs.get_structures()[0]
59 except:
60 raise ValueError('could not get structure from text of CIF file')
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
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
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
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
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
126 """
127 try:
128 cstruct = read_cif_structure(ciftext)
129 except ValueError:
130 return '# could not read CIF file'
132 sgroup = SpacegroupAnalyzer(cstruct).get_symmetry_dataset()
133 space_group = sgroup["international"]
135 if isinstance(absorber, int):
136 absorber = atomic_symbol(absorber_z)
137 absorber_z = atomic_number(absorber)
139 if edge is None:
140 edge = 'K' if absorber_z < 58 else 'L3'
142 edge_energy = xray_edge(absorber, edge).energy
143 edge_comment = f'{absorber:s} {edge:s} edge, around {edge_energy:.0f} eV'
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)
151 atoms_map = {}
152 for i, atom in enumerate(unique_pot_atoms):
153 atoms_map[atom] = i + 1
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})')
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
178 if site_index is not None:
179 absorber_index = site_index - 1
181 # print("Got sites ", len(cstruct.sites), len(site_atoms), len(site_tags))
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}']
189 for i, site_dist in enumerate(sphere):
190 s_index = site_dist[0].index
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)
198 out_text = ['*** feff input generated by xraylarch cif2feff using pymatgen ***']
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)
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}')
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}')
220 out_text.extend(['* ', '', ''])
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')
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}')
239 out_text.extend(['', 'EXCHANGE 0', '',
240 '* POLARIZATION 0 0 0', '',
241 'POTENTIALS', '* IPOT Z Tag'])
243 # loop to find atoms actually in cluster, in case some atom
244 # (maybe fractional occupation) is not included
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
259 dist = cluster.get_distance(0, i+1)
260 at_lines.append((dist, site.x, site.y, site.z, ipot, sym, tags[i+1]))
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}')
269 out_text.append('')
270 out_text.append('ATOMS')
271 out_text.append(f'* x y z ipot tag distance site_info')
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}')
281 out_text.append('')
282 out_text.append('* END')
283 out_text.append('')
284 return strict_ascii('\n'.join(out_text))