feffdb 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
feffdb/utils.py ADDED
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ very simple interface to CIF file for Feff DB
4
+ """
5
+ import json
6
+ import os
7
+ import shutil
8
+ import sqlite3
9
+ import re
10
+ from io import IOBase
11
+ from charset_normalizer import from_bytes
12
+ from pathlib import Path
13
+ from platformdirs import user_data_dir
14
+ from warnings import warn
15
+ from xraydb import XrayDB, atomic_mass, atomic_symbol
16
+ from xraydb.chemparser import chemparse
17
+
18
+ from pymatgen.io.cif import CifParser
19
+
20
+ from .schema import feffdb_schema, VERSIONS
21
+
22
+ FEFFDB_NAME = 'feffdat.db'
23
+ FEFFDB_ENV = 'FEFFDAT_DB'
24
+ LARCHDIR_ENV = 'LARCHDIR '
25
+ LARCHDIR_DEF = '.larch'
26
+
27
+
28
+ def is_feffdb(db: str|sqlite3.Connection) -> bool:
29
+ """test if database file or open connection is a valid Feffdata DB
30
+
31
+ a valid FEFFDB file has tables 'feffdat', 'feffinp', 'cif', 'element', and 'info'
32
+ """
33
+
34
+ feffdb_tables = ("element", "info", "feffdat", "feffinp", "cif")
35
+ if isinstance(db, sqlite3.Connection):
36
+ dbconn = db
37
+ else:
38
+ try:
39
+ dbconn = sqlite3.connect(db)
40
+ except Exception:
41
+ return False
42
+
43
+ cursor = dbconn.cursor()
44
+ cursor.execute("select name from sqlite_master where type='table';")
45
+ tables = [row[0] for row in cursor.fetchall()]
46
+ return all([tab in tables for tab in feffdb_tables])
47
+
48
+
49
+ def get_feffdb(dbname:str|None=None) -> Path:
50
+ """return Path to FEFF DB file
51
+
52
+ 1. if the passed dbname is a valid FeffDB, that will be used.
53
+ 2. if the Environmental Variable FEFFDAT_DB is set (and is a valid FeffDB),
54
+ that will be used.
55
+ 4. Use `platformsdir.user_data_dir()` to find a `feffdb` folder.
56
+ if `feffdat.db` exists there, use that.
57
+ 5. Try to create `platformsdir.user_data_dir('feffdb')` and copy
58
+ the feffdat.db there, and use that if it suceeeds.
59
+
60
+ Finally, if no valid FEFF DB can be found a ValueError will be raised.
61
+ """
62
+ if dbname is None:
63
+ dbname = os.environ.get(FEFFDB_ENV, None)
64
+ if is_feffdb(dbname):
65
+ return Path(dbname)
66
+
67
+ parent = Path(user_data_dir('feffdb'))
68
+ parent.mkdir(mode=0o755, exist_ok=True)
69
+ dbpath = parent / FEFFDB_NAME
70
+ if is_feffdb(dbpath):
71
+ return dbpath
72
+
73
+ else:
74
+ here = Path(__file__).parent / FEFFDB_NAME
75
+ try:
76
+ shutil.copy(here, dbpath)
77
+ if is_feffdb(dbpath):
78
+ return dbpath
79
+ except Exception:
80
+ warn(f'could not install {FEFFDB_NAME} to {parent}')
81
+
82
+ raise ValueError("cannot find a valid Feff Database")
83
+
84
+
85
+ def create_feffdb(dbname=None):
86
+ """create FeffData.DB"""
87
+ if dbname is None:
88
+ dbpath = get_feffdb()
89
+ else:
90
+ dbpath = Path(dbname)
91
+ if dbpath.exists():
92
+ raise IOError(f"file {dbname} already exists")
93
+ dbpath.parent.mkdir(parents=True, mode=0o755, exist_ok=True)
94
+
95
+ conn = sqlite3.connect(dbpath)
96
+ c = conn.cursor()
97
+ for t in feffdb_schema.split(';'):
98
+ c.execute(t)
99
+
100
+ for version_dat in VERSIONS:
101
+ c.execute('insert into version values (?, ?, ?, ?)', version_dat)
102
+
103
+ xdb = XrayDB()
104
+ eltab = xdb.tables['elements']
105
+ q = []
106
+ for row in xdb.query(eltab).all():
107
+ c.execute('insert into element values (?, ?, ?, ?)',
108
+ (row.atomic_number, row.element, row.name, row.molar_mass))
109
+ conn.commit()
110
+
111
+
112
+ def read_textfile(filename):
113
+ """read text from a file as string
114
+
115
+ Argument
116
+ --------
117
+ filename (str or file): name of file to read or file-like object
118
+
119
+ Returns
120
+ -------
121
+ text of file as string.
122
+
123
+ Notes
124
+ ------
125
+ 1. the encoding is detected with charset_normalizer.from_bytes
126
+ which is then used to decode bytes read from file.
127
+ 2. line endings are normalized to be '\n'.
128
+ """
129
+
130
+ text = ''
131
+ def decode(bytedata):
132
+ return str(from_bytes(bytedata).best())
133
+
134
+ if isinstance(filename, IOBase):
135
+ text = filename.read()
136
+ if filename.mode == 'rb':
137
+ text = decode(text)
138
+ else:
139
+ with open(Path(filename), 'rb') as fh:
140
+ text = decode(fh.read())
141
+ return text.replace('\r\n', '\n').replace('\r', '\n')
142
+
143
+
144
+ def get_filename_and_text(fname):
145
+ if isinstance(fname, Path):
146
+ fname = fname.absolute().resolve().as_posix()
147
+ text = read_textfile(fname)
148
+ elif isinstance(fname, str) and len(fname) < 255:
149
+ fname = Path(fname).absolute().resolve().as_posix()
150
+ text = read_textfile(fname)
151
+ else:
152
+ text = fname
153
+ fname = '<unknown>'
154
+ return fname, text
155
+
156
+ def parse_cif(ciffile):
157
+ ciffile, ciftext = get_filename_and_text(ciffile)
158
+
159
+ try:
160
+ cif = CifParser.from_str(ciftext, occupancy_tolerance=10, site_tolerance=0.005)
161
+ except Exception:
162
+ raise ValueError(f'invalid CIF {ciffile}')
163
+
164
+ cifkey = list(cif._cif.data.keys())[0]
165
+ dat = cif._cif.data[cifkey].data
166
+
167
+ formula = None
168
+ for formname in ('_chemical_formula_sum', '_chemical_formula_moiety'):
169
+ if formname in dat:
170
+ try:
171
+ parsed_formula = chemparse(dat[formname])
172
+ formula = dat[formname].replace(' ', '')
173
+ except:
174
+ print("Could not parse ", dat[formname])
175
+
176
+ if formula is None and '_atom_site_type_symbol' in dat:
177
+ comps = {}
178
+ complist = dat['_atom_site_type_symbol']
179
+ for c in complist:
180
+ if c not in comps:
181
+ nx = complist.count(c)
182
+ comps[c] = '%s%d' % (c, nx) if nx != 1 else c
183
+ formula = ''.join(comps.values())
184
+
185
+ if formula is None:
186
+ raise ValueError(f'Cannot read chemical formula from CIF {ciffile}')
187
+
188
+ # compound
189
+ compound = '<missing>'
190
+ for compname in ('_chemical_compound_source',
191
+ '_chemical_name_systematic',
192
+ '_chemical_name_common',
193
+ '_chemical_name_mineral'):
194
+ if compname in dat:
195
+ compound = dat[compname]
196
+
197
+ # get spacegroup and symmetry
198
+ sgroup_name = dat.get('_symmetry_space_group_name_H-M', None)
199
+ if sgroup_name is None:
200
+ for key, val in dat.items():
201
+ if 'space_group' in key and 'H-M' in key:
202
+ sgroup_name = val
203
+
204
+ source_db = ''
205
+ source_id = -1
206
+ amcsd_id = dat.get('_database_code_amcsd', None)
207
+ if amcsd_id is not None:
208
+ source_db = 'amcsd'
209
+ source_id = amcsd_id
210
+
211
+ return {'text': ciftext, 'filename': ciffile,
212
+ 'formula': formula, 'compound': compound,
213
+ 'space_group': sgroup_name,
214
+ 'source_db': source_db, 'source_id': source_id}
215
+
216
+
217
+ def parse_feffinp(feffinp):
218
+ """parse feff.inp just enough for FeffDB"""
219
+
220
+ feffinp, text = get_filename_and_text(feffinp)
221
+
222
+ edge = ''
223
+ absorber = 0
224
+ ipots = []
225
+ natoms = 0
226
+ mode = None
227
+ for line in text.split('\n'):
228
+ line = line.strip()
229
+ if line.startswith('*') or line.startswith('#'):
230
+ continue
231
+
232
+ if line.lower().startswith('edge'):
233
+ words = line.split(' ')
234
+ edge = words[1].upper()
235
+ elif line.lower().startswith('potent'):
236
+ mode = 'pot'
237
+ elif line.lower().startswith('atoms'):
238
+ mode = 'atoms'
239
+ elif line.lower().startswith('end'):
240
+ mode = None
241
+ elif mode == 'pot':
242
+ if line.startswith('0'):
243
+ words = line.strip().split()
244
+ absorber = int(words[1])
245
+ elif len(line) > 6:
246
+ words = line.strip().split()
247
+ ipots.append(int(words[1]))
248
+ elif mode == 'atoms':
249
+ try:
250
+ words = line.strip().split()
251
+ w = [float(x) for x in words[:4]]
252
+ natoms += 1
253
+ except (ValueError, TypeError):
254
+ pass
255
+
256
+ return {'text': text, 'filename': feffinp, 'absorber': absorber,
257
+ 'edge': edge, 'scatterers': json.dumps(ipots), 'natoms': natoms}
258
+
259
+
260
+ def parse_feffdat(feffdatfile):
261
+ """parse feffdat just enough for FeffDB"""
262
+ feffdatfile, fefftext = get_filename_and_text(feffdatfile)
263
+
264
+ mode = 'header'
265
+ potentials, geom, data = [], [],[]
266
+ version, pcounter, iline = '', 0, 0
267
+ reff = 0.0
268
+ lines = fefftext.split('\n')
269
+
270
+ if 'feff' not in lines[0].lower()[:]:
271
+ raise ValueError(f'Not a valid feff.dat file: {feffdatfile}')
272
+
273
+ for line in lines:
274
+ iline += 1
275
+ line = line[:-1].strip()
276
+ if line.startswith('#'):
277
+ line = line[1:].strip()
278
+
279
+ if iline == 1:
280
+ title = line[:64].strip()
281
+ version = line[64:].strip()
282
+ continue
283
+ if line.startswith('k') and 'mag[feff]' in line:
284
+ mode = 'arrays'
285
+ break
286
+ elif '----' in line[2:10]:
287
+ mode = 'path'
288
+ continue
289
+ #
290
+ if (mode == 'header' and
291
+ (re.match(r'^Abs\b', line) or re.match(r'^Pot\s+\d+\b', line)) and
292
+ re.search(r'\bZ\s*=', line)):
293
+ words = line.replace('=', ' ').split()
294
+ ipot, z, rmt, rnm = (0, 0, 0, 0)
295
+ words.pop(0)
296
+ if re.match(r'^Pot\s+\d+\b', line):
297
+ ipot = int(words.pop(0))
298
+ iz = int(words[1])
299
+ rmt = float(words[3])
300
+ rnm = float(words[5])
301
+ if re.match(r'^Abs\b', line):
302
+ edge = words[6]
303
+ potentials.append((ipot, iz, rmt, rnm))
304
+ elif mode == 'header' and re.match(r'^Gam_ch\s*=', line):
305
+ pass
306
+ elif mode == 'header' and re.match(r'^Mu\s*=', line):
307
+ pass
308
+ elif mode == 'path':
309
+ pcounter += 1
310
+ if pcounter == 1:
311
+ w = [float(x) for x in line.split()[:5]]
312
+ nleg = int(w.pop(0))
313
+ degen, reff, rnorman, e_edge = w
314
+ elif pcounter > 2:
315
+ words = line.split()
316
+ xyz = [float(x) for x in words[:3]]
317
+ ipot = int(words[3])
318
+ iz = int(words[4])
319
+ if len(words) > 5:
320
+ lab = words[5]
321
+ else:
322
+ lab = atomic_symbol(iz)
323
+ amass = atomic_mass(iz)
324
+ this_geom = [lab, iz, ipot, amass] + xyz
325
+ if len(geom) == 0:
326
+ absorber = lab
327
+ geom.append(tuple(this_geom))
328
+ elif mode == 'arrays':
329
+ break
330
+
331
+ if len(geom) < 2:
332
+ raise ValueError(f'could not read Path from feff.dat file: {feffdatfile}')
333
+
334
+ absorber = geom[0][1]
335
+ scatterer = geom[1][1]
336
+
337
+ return {'text': fefftext, 'filename': feffdatfile,
338
+ 'absorber': absorber, 'scatterer': scatterer,
339
+ 'reff': reff, 'degen': degen, 'nleg': nleg,
340
+ 'edge': edge, 'geometry': json.dumps(geom)}
341
+
342
+
343
+
344
+ if __name__ == '__main__':
345
+ print("CIF")
346
+ out = parse_cif('../fayalite.cif')
347
+ print(out.keys())
348
+ print(len(out['text']))
349
+ for key, val in out.items():
350
+ if key != 'text':
351
+ print(key, val)
352
+
353
+ print("FEFF:")
354
+ out = parse_feffdat('../feff0001.dat')
355
+ print(out.keys())
356
+ print(len(out['text']))
357
+ for key, val in out.items():
358
+ if key != 'text':
359
+ print(key, val)
feffdb/version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1'
22
+ __version_tuple__ = version_tuple = (0, 1)
23
+
24
+ __commit_id__ = commit_id = 'ga71d582ee'
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: feffdb
3
+ Version: 0.1
4
+ Summary: Sqlite Database for Feff.dat files, especially for 1st shell EXAFS
5
+ Author-email: Matthew Newville <matt.newville@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/xraypy/feffdb
8
+ Project-URL: Documentation, https://xraypy.github.io/feffdb
9
+ Project-URL: Tracker, https://github.com/xraypy/feffdb/issues
10
+ Keywords: Feff,EXAFS
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Topic :: Scientific/Engineering :: Visualization
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Programming Language :: Python :: 3.15
26
+ Requires-Python: >=3.11
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: xraydb
30
+ Requires-Dist: numpy>=2.2
31
+ Requires-Dist: scipy
32
+ Requires-Dist: charset_normalizer
33
+ Requires-Dist: sqlalchemy>=2
34
+ Requires-Dist: pymatgen>=2025.6.14
35
+ Requires-Dist: pyshortcuts>=1.9.8
36
+ Requires-Dist: tabulate
37
+ Provides-Extra: dev
38
+ Requires-Dist: build; extra == "dev"
39
+ Requires-Dist: twine; extra == "dev"
40
+ Provides-Extra: doc
41
+ Requires-Dist: sphinx; extra == "doc"
42
+ Requires-Dist: sphinx-copybutton; extra == "doc"
43
+ Requires-Dist: sphinx-design; extra == "doc"
44
+ Requires-Dist: sphinxcontrib-video; extra == "doc"
45
+ Requires-Dist: jupyter_sphinx; extra == "doc"
46
+ Requires-Dist: nbsphinx; extra == "doc"
47
+ Provides-Extra: test
48
+ Requires-Dist: pytest; extra == "test"
49
+ Requires-Dist: pytest-cov; extra == "test"
50
+ Requires-Dist: coverage; extra == "test"
51
+ Provides-Extra: all
52
+ Requires-Dist: feffdb[dev,doc,test]; extra == "all"
53
+ Dynamic: license-file
54
+
55
+ # FeffDB
56
+
57
+ Feff DB is an SQLite3 database and Python library for the results of
58
+ Feff EXAFS calculations, and some support code to read and work with
59
+ this database and the data it contains. The initial motivation is to
60
+ provide "first-shell EXAFS" calculations for many common ligands or
61
+ atom pairs, so that these can be browsed and re-used without having to
62
+ setup and run Feff for many common cases.
@@ -0,0 +1,15 @@
1
+ feffdb/__init__.py,sha256=07HjHdlM1FIAMz9OjA4DsIe_AuL9c-TEcPKyRchtY7I,323
2
+ feffdb/apps.py,sha256=cLhJUVudaqxilYRBe7YeIrpqwjcZKi4rmu3gx7s_d_g,2817
3
+ feffdb/feffdat.db,sha256=5dYiqC3Le7esrMFW_hRYyYACChPPQL0UHlJvWDsDYbs,1662976
4
+ feffdb/feffdb.py,sha256=de_NEqCFPcvHoRPCl2_NTySjoyFJIBXwmwABbR1bdsU,12087
5
+ feffdb/feffpath.py,sha256=oIR8ZBqX6JF5UaPiwYZ4kwezy63I0kBvj5w9Ra3XmLw,19040
6
+ feffdb/schema.py,sha256=oDAlV_uDTWKU6WqXM1ANufDYRJ2TIQ4EL4DNviYMnLk,3224
7
+ feffdb/simpledb.py,sha256=CQg8LcmtyVoHAcJImfqxfzKJd4DqrD1KkYFzx3HX6D8,12547
8
+ feffdb/utils.py,sha256=FQwRjM4vQTchp5dzEevgvyA84HjJmEYl8duDFKQW9rU,11068
9
+ feffdb/version.py,sha256=dG56YXWdUYnaF4XVp7VDuwKai6JwmgaruKwoXv7wy0k,523
10
+ feffdb-0.1.dist-info/licenses/LICENSE,sha256=NnCHEmQk-hVaiYelyQ2olHgC2-ixV_alvCBvYjNVbQU,1359
11
+ feffdb-0.1.dist-info/METADATA,sha256=3pSgIKoNjAk-HhBcp7mXFJYLa2EAXTS0a_8ee_r9ex4,2486
12
+ feffdb-0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
13
+ feffdb-0.1.dist-info/entry_points.txt,sha256=QLUHJcPcZLQjt1O_eZ6emH2PfUCNzWvTvd7h5V6VCXY,50
14
+ feffdb-0.1.dist-info/top_level.txt,sha256=5lUQJfFqNkLACCVftnG7Vvb8WECZ-4DYxTPWyES4lCI,7
15
+ feffdb-0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ feffdb = feffdb.apps:feffdb_cli
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FeffDB development team, including
4
+ - Samantha Liao, Cornell University
5
+ - Juanjuan Huang, Argonne National Laboratory
6
+ - Nina Andrejevic, Argonne National Laboratory
7
+ - Yanna Chen, Canadian Light Source
8
+ - Matthew Newville, The University of Chicago
9
+ - Shelly Kelly, Argonne National Laboratory
10
+
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ feffdb