feffdb 0.1__tar.gz

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-0.1/LICENSE ADDED
@@ -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.
feffdb-0.1/MANIFEST.in ADDED
@@ -0,0 +1,10 @@
1
+ include README.md LICENSE AI_POLICY.md INSTALL MANIFEST.in setup.py pyproject.toml
2
+ exclude *.pyc core.* *~ *.pdf .deps .gitignore
3
+ recursive-include feffdb *
4
+ recursive-exclude feffdb/__pycache__ *
5
+ recursive-exclude tests *
6
+ recursive-exclude examples *
7
+ recursive-exclude doc *
8
+ recursive-exclude source_data *
9
+ recursive-exclude sandbox *
10
+ recursive-exclude doc/_build *
feffdb-0.1/PKG-INFO ADDED
@@ -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.
feffdb-0.1/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # FeffDB
2
+
3
+ Feff DB is an SQLite3 database and Python library for the results of
4
+ Feff EXAFS calculations, and some support code to read and work with
5
+ this database and the data it contains. The initial motivation is to
6
+ provide "first-shell EXAFS" calculations for many common ligands or
7
+ atom pairs, so that these can be browsed and re-used without having to
8
+ setup and run Feff for many common cases.
@@ -0,0 +1,7 @@
1
+ from .utils import get_feffdb, is_feffdb, create_feffdb
2
+ from .feffdb import FeffDatabase
3
+ from .feffpath import feffpath, FeffDatFile
4
+ from .version import __version__, version_tuple
5
+
6
+ __all__ = ['get_feffdb', 'is_feffdb', 'create_feffdb', 'feffpath',
7
+ 'FeffDatabase', 'FeffDatFile', '__version__', 'version_tuple']
@@ -0,0 +1,77 @@
1
+ from argparse import ArgumentParser
2
+
3
+ from pathlib import Path
4
+ from tabulate import tabulate
5
+ import xraydb
6
+ from .utils import get_feffdb, create_feffdb
7
+ from .feffdb import FeffDatabase
8
+
9
+
10
+ VALID_ELEMS = [el.element for el in xraydb.get_xraydb().get_cache('elements')]
11
+ VALID_ELEMS.extend(['None', 'none', 'All', 'all'])
12
+
13
+ def feffdb_cli():
14
+ """
15
+ feffdb command-line app
16
+ """
17
+ dbname_default = get_feffdb().absolute().as_posix()
18
+ parser = ArgumentParser(description='Feff Database')
19
+ parser.add_argument('-n', '--name', dest='dbname',
20
+ default=None, help=f"database to use [{dbname_default}]" )
21
+ parser.add_argument('-c', '--create', action='store_true', default=False,
22
+ help="create new database")
23
+ parser.add_argument('--rmin', default=0,
24
+ help='minimum distance (Ang)')
25
+ parser.add_argument('--rmax', default=10,
26
+ help='maximum distance (Ang)')
27
+ parser.add_argument('absorber', nargs='?',
28
+ help='symbol for absorbing element (use "all" for all absorbers)')
29
+ parser.add_argument('scatterer', nargs='?',
30
+ help='symbol for scattering element')
31
+
32
+ args = parser.parse_args()
33
+
34
+ dbname = args.dbname or dbname_default
35
+
36
+ if Path(dbname).exists() and args.create:
37
+ print(f"database {dbname} already exists!")
38
+
39
+ if not Path(dbname).exists():
40
+ if args.create:
41
+ print("will create db ", dbname)
42
+ create_feffdb(name=dbname)
43
+ return
44
+ else:
45
+ print(f"database {dbname} does not exist, use '-c' to create")
46
+ return
47
+
48
+ feffdb = FeffDatabase(dbname, create_ok=False)
49
+ npaths = feffdb.list_feffdat()
50
+ if npaths is None or len(npaths) < 1:
51
+ print(f"database {dbname} exists but is empty")
52
+ return
53
+
54
+
55
+
56
+ absorber = args.absorber
57
+ if absorber not in VALID_ELEMS:
58
+ print(f"Unknown absorber: {absorber}, Must be valid atomic symbol or 'All' or 'None'")
59
+
60
+ scatterer = args.scatterer
61
+ if scatterer is not None and scatterer not in VALID_ELEMS:
62
+ print(f"Unknown scatterer: {scatterer} Must be valid atomic symbol or empty or 'All' or 'None'")
63
+
64
+ out = []
65
+ if absorber is not None or scatterer is not None:
66
+ if scatterer in ('All', 'all', 'None', 'none'):
67
+ scatterer = None
68
+ if absorber in ('All', 'all', 'None', 'none'):
69
+ absorber = None
70
+ rows = feffdb.list_feffdat(absorber=absorber, scatterer=scatterer,
71
+ rmin=float(args.rmin), rmax=float(args.rmax).
72
+ with_geometry=False)
73
+ out.append(row)
74
+ if len(out) > 0:
75
+ print(tabulate(out, headers='keys', tablefmt='psql'))
76
+ else:
77
+ parser.print_usage()
Binary file
@@ -0,0 +1,318 @@
1
+ #!/bin/env python
2
+ '''
3
+ This script creates an SQLite3 database for Feff Data
4
+ '''
5
+ import json
6
+ import lzma
7
+ from pathlib import Path
8
+ from pyshortcuts import hash_password, check_password_rules
9
+ from xraydb import atomic_number, atomic_symbol
10
+
11
+
12
+ from .simpledb import SimpleDB
13
+ from .utils import (get_feffdb, create_feffdb,
14
+ parse_cif, parse_feffinp, parse_feffdat)
15
+
16
+ def compress(val):
17
+ return lzma.compress(val.encode('utf-8'))
18
+
19
+ def decompress(val):
20
+ return lzma.decompress(val).decode('utf-8')
21
+
22
+ class FeffDatabase(SimpleDB):
23
+ def __init__(self, dbname=None, create_ok=False, read_only=False):
24
+ if dbname is not None:
25
+ dbpath = Path(dbname)
26
+ else:
27
+ try:
28
+ dbpath = get_feffdb()
29
+ except ValueError:
30
+ dbpath = None
31
+
32
+ if (dbpath is None or not dbpath.exists()) and create_ok:
33
+ create_feffdb(dbname=dbname)
34
+
35
+ self.dbname = dbpath.absolute().as_posix()
36
+ SimpleDB.__init__(self, dbname=self.dbname, server='sqlite',
37
+ read_only=read_only)
38
+
39
+ def __repr__(self):
40
+ return f"FeffDatabase('{self.dbname}')"
41
+
42
+
43
+ def __addrow(self, table, data):
44
+ """add row to tablle, or return existing row with data"""
45
+ # first look if this already exists
46
+ row = self.get_rows(table, where=data,
47
+ limit_one=True, none_if_empty=True)
48
+
49
+ if row is None:
50
+ self.insert(table, **data)
51
+ row = self.get_rows(table, where=data,
52
+ limit_one=True, none_if_empty=True)
53
+ if row is None:
54
+ return None
55
+ return row.id
56
+
57
+
58
+ def add_cif_file(self, cif_file):
59
+ """add a CIF file to the database"""
60
+ try:
61
+ out = parse_cif(cif_file)
62
+ except ValueError:
63
+ print(f"error parsing {cif_file}")
64
+ return None
65
+
66
+ text = compress(out.pop('text'))
67
+ fname = out.pop('filename')
68
+ dbargs = {k:v for k, v in out.items()}
69
+ dbargs['ciftext'] = text
70
+ if dbargs['source_db'] is None and fname is not None:
71
+ dbargs['source_db'] = fname
72
+
73
+ this_id = self.__addrow('cif', dbargs)
74
+ if this_id is None:
75
+ raise ValueError(f'could not add CIF: {cif_file}')
76
+ return this_id
77
+
78
+
79
+ def add_feffinp(self, feff_inp, cif_file=None):
80
+ """add a Feff input file to the database"""
81
+ cif_id = 0
82
+ if cif_file is not None:
83
+ cif_id = self.add_cif_file(cif_file)
84
+
85
+ try:
86
+ finp = parse_feffinp(feff_inp)
87
+ except ValueError:
88
+ raise ValueError(f'error parsing feffinp : {feff_inp}')
89
+
90
+ text = compress(finp.pop('text'))
91
+ finp.pop('filename')
92
+ dbargs = {k:v for k, v in finp.items()}
93
+ dbargs['inptext'] = text
94
+ dbargs['cif_id'] = cif_id
95
+
96
+ this_id = self.__addrow('feffinp', dbargs)
97
+ if this_id is None:
98
+ raise ValueError(f'could not add feffinp : {feff_inp}')
99
+ return this_id
100
+
101
+ def add_feffdat(self, feffdat, feff_inp, cif_file=None,
102
+ label=None, description=None,
103
+ avoid_duplicates=True, rtoler=0.015):
104
+ """add a Feff.dat file to the database"""
105
+
106
+ finp_id = self.add_feffinp(feff_inp, cif_file=cif_file)
107
+
108
+ try:
109
+ dat = parse_feffdat(feffdat)
110
+ except ValueError:
111
+ raise ValueError(f'error parsing feff.dat file: {feffdat}')
112
+
113
+ text = compress(dat.pop('text'))
114
+ fname = dat.pop('filename')
115
+
116
+ if avoid_duplicates:
117
+ nearby = self.list_feffdat(absorber=dat['absorber'],
118
+ scatterer=dat['scatterer'],
119
+ edge=dat['edge'],
120
+ rmin=float(dat['reff'])-rtoler,
121
+ rmax=float(dat['reff'])+rtoler)
122
+ if len(nearby) > 0:
123
+ print("Avoiding Duplicate entries:")
124
+ s = nearby[0]
125
+ print('This: ', dat['absorber'], dat['scatterer'], dat['edge'], dat['reff'])
126
+ print(s['id'], s['label'], s['absorber'], s['scatterer'], s['edge'], s['reff'])
127
+ return
128
+
129
+ if description is None:
130
+ # default description is filenamae and a few parent folders
131
+ words = Path(fname).parts
132
+ n = min(len(words), 2)
133
+ description = Path(*words[-n:]).as_posix()
134
+ if label is None:
135
+ # default label combines absorber, scatterer, reff
136
+ words = [atomic_symbol(dat['absorber']),
137
+ atomic_symbol(dat['scatterer']),
138
+ str(round(100*dat['reff']))]
139
+ label = ''.join(words)
140
+
141
+
142
+ if label is not None:
143
+ # check fie repeated rows
144
+ rows = self.get_rows('feffdat', where={'label': label},
145
+ none_if_empty=True)
146
+ if rows is not None:
147
+ words = [atomic_symbol(dat['absorber']),
148
+ atomic_symbol(dat['scatterer']),
149
+ str(round(100*dat['reff'])), '_', dat['edge']]
150
+ label = ''.join(words)
151
+ rows = self.get_rows('feffdat', where={'label': label},
152
+ none_if_empty=True)
153
+
154
+ if rows is not None:
155
+ olabel, i = label, 0
156
+ while rows is not None:
157
+ i += 1
158
+ label = f'{olabel}_{i}'
159
+ rows = self.get_rows('feffdat', where={'label': label},
160
+ none_if_empty=True)
161
+
162
+
163
+ dbargs = {k:v for k, v in dat.items()}
164
+ dbargs['feffdat'] = text
165
+ dbargs['feffinp_id'] = finp_id
166
+ dbargs['label'] = label
167
+ dbargs['description'] = description
168
+
169
+ this_id = self.__addrow('feffdat', dbargs)
170
+ if this_id is None:
171
+ raise ValueError(f'could not add feff data: {feffdat}')
172
+ return this_id
173
+
174
+ def get_feffdat(self, feffid):
175
+ """get text of feff.dat, either by table 'id' or
176
+ or by 'label' in feffdat table
177
+ """
178
+ try:
179
+ where = {'id': int(feffid)}
180
+ except ValueError:
181
+ where = {'label': feffid}
182
+
183
+ row = self.get_rows('feffdat', where=where,
184
+ limit_one=True, none_if_empty=True)
185
+ if row is not None:
186
+ return decompress(row.feffdat)
187
+
188
+ def get_feffinp(self, feffinp_id):
189
+ "get text of feff.inp by id in feffinp table"
190
+ row = self.get_rows('feffinp', where={'id': feffinp_id},
191
+ limit_one=True, none_if_empty=True)
192
+ if row is not None:
193
+ return decompress(row.inptext)
194
+
195
+ def get_feffinp_for_feffdat(self, feffdat_id):
196
+ "get text of feff.inp for a Feff.dat file"
197
+ row = self.get_rows('feffdat', where={'id': feffdat_id},
198
+ limit_one=True, none_if_empty=True)
199
+ if row is not None:
200
+ trow = self.get_rows('feffinp', where={'id': row.feffinp_id},
201
+ limit_one=True, none_if_empty=True)
202
+ if trow is not None:
203
+ return decompress(trow.inptext)
204
+
205
+ def get_cifinp_for_feffinp(self, feffinp_id):
206
+ "get text of CIF for a feff.inp file"
207
+ row = self.get_rows('feffinp', where={'id': feffinp_id},
208
+ limit_one=True, none_if_empty=True)
209
+ if row is not None:
210
+ trow = self.get_rows('cif', where={'id': row.cif_id},
211
+ limit_one=True, none_if_empty=True)
212
+ if trow is not None:
213
+ return decompress(trow.ciftext)
214
+
215
+ def list_feffdat(self, absorber=None, scatterer=None, edge=None,
216
+ rmin=0., rmax=20.0, with_geometry=True):
217
+ """get a list of feff.dat files matching on one or more criteria:
218
+
219
+ Arguments
220
+ ------------
221
+ absorber (None or str) symbol for absorber [None for all]
222
+ scatterer (None or str) symbol for scatterer [None for all]
223
+ edge (None or str) string for absorption edge [None for all]
224
+ rmin float minimum R value [0.0]
225
+ rmax float maximum R value [20.0]
226
+ with_geometry bool whether to include 'geometry' [True]
227
+ Returns
228
+ --------
229
+ list of path dictionaries
230
+ """
231
+ wargs = {}
232
+ if absorber is not None:
233
+ if isinstance(absorber, str):
234
+ absorber = atomic_number(absorber.title())
235
+ wargs['absorber'] = absorber
236
+ if scatterer is not None:
237
+ if isinstance(scatterer, str):
238
+ scatterer = atomic_number(scatterer.title())
239
+ wargs['scatterer'] = scatterer
240
+ if edge is not None:
241
+ wargs['edge'] = edge.title()
242
+
243
+ rows = self.get_rows('feffdat', where=wargs, limit_one=False)
244
+ out = []
245
+ for row in rows:
246
+ if row.reff > rmin and row.reff < rmax:
247
+ formula = '?'
248
+ finp = self.get_rows('feffinp', where={'id': row.feffinp_id},
249
+ limit_one=True, none_if_empty=True)
250
+ if finp is not None and finp.cif_id > 0:
251
+ cif = self.get_rows('cif', where={'id': finp.cif_id},
252
+ limit_one=True, none_if_empty=True)
253
+ if cif is not None:
254
+ formula = cif.formula
255
+ compound = cif.compound.lower()
256
+
257
+ rowdat = {'id': row.id,
258
+ 'label': row.label,
259
+ 'absorber': atomic_symbol(row.absorber),
260
+ 'scatterer': atomic_symbol(row.scatterer),
261
+ 'edge': row.edge,
262
+ 'reff': row.reff,
263
+ 'degen': row.degen,
264
+ 'formula': formula,
265
+ 'compound': compound,
266
+ 'description': row.description}
267
+ if with_geometry:
268
+ rowdat['geometry'] = json.loads(row.geometry)
269
+ out.append(rowdat)
270
+ return out
271
+
272
+
273
+ def add_person(self, email, name=None, password=''):
274
+ """add a person (email, optional name) to feffdat table:
275
+ used for ratings and to identify added feff.dat files
276
+ """
277
+ dbargs = {'email': email}
278
+ row = self.get_rows('person', where=dbargs,
279
+ limit_one=True, none_if_empty=True)
280
+ if row is not None:
281
+ if name is not None and name != row.name:
282
+ self.update('person', where=dbargs, name=name)
283
+ else:
284
+ print(f"warning person {email} already exisis")
285
+ else:
286
+ if name is not None:
287
+ dbargs['name'] = name
288
+ if password is None:
289
+ password = ''
290
+ if len(password) > 1:
291
+ password_ok = check_password_rules(password)
292
+ if not password_ok:
293
+ raise ValueError('Password is invalid')
294
+ dbargs['password'] = hash_password(password)
295
+
296
+ self.__addrow('person', dbargs)
297
+
298
+ def get_person(self, email):
299
+ """get person by email"""
300
+ return self.get_rows('person', where={'email': email},
301
+ limit_one=True, none_if_empty=True)
302
+
303
+ def check_person_password(self, email, password):
304
+ """check validity of a person's password"""
305
+ # if len(person
306
+
307
+ def get_feffdat_ratings(self, feffdat_id=None, email=None):
308
+ """get all ratings for feff.dat file either by feffdat_id or by email
309
+ """
310
+ args = {}
311
+ if feffdat_id is not None:
312
+ args['feffdat_id'] = feffdat_id
313
+ if email is not None:
314
+ person_row = self.get_person(email)
315
+ if person_row is not None:
316
+ args['person_row'] = person_row.id
317
+
318
+ return self.get_rows('feffdat_ratings', where=args)