Coverage for /Users/Newville/Codes/xraylarch/larch/io/xdi.py: 15%
176 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
1#!/usr/bin/env python
2"""
3Read/Write XAS Data Interchange Format for Python
4"""
5import os
6from ctypes import c_long, c_double, c_char_p, c_void_p, pointer, Structure
8__version__ = '1.2.0larch'
10from numpy import array, exp, log, sin, arcsin
12from .. import Group
13from ..larchlib import get_dll
14from ..utils.strutils import bytes2str, str2bytes
15from ..utils.physical_constants import RAD2DEG, PLANCK_HC
17class XDIFileStruct(Structure):
18 "emulate XDI File"
19 _fields_ = [('nmetadata', c_long),
20 ('narrays', c_long),
21 ('npts', c_long),
22 ('narray_labels', c_long),
23 ('nouter', c_long),
24 ('error_lineno', c_long),
25 ('dspacing', c_double),
26 ('xdi_libversion',c_char_p),
27 ('xdi_version', c_char_p),
28 ('extra_version', c_char_p),
29 ('filename', c_char_p),
30 ('element', c_char_p),
31 ('edge', c_char_p),
32 ('comments', c_char_p),
33 ('error_line', c_char_p),
34 ('error_message', c_char_p),
35 ('array_labels', c_void_p),
36 ('outer_label', c_char_p),
37 ('array_units', c_void_p),
38 ('meta_families', c_void_p),
39 ('meta_keywords', c_void_p),
40 ('meta_values', c_void_p),
41 ('array', c_void_p),
42 ('outer_array', c_void_p),
43 ('outer_breakpts', c_void_p)]
45string_attrs = ('comments', 'edge', 'element', 'error_line',
46 'error_message', 'extra_version', 'filename',
47 'outer_label', 'xdi_libversion', 'xdi_pyversion',
48 'xdi_version')
51def tostr(val):
52 if isinstance(val, str):
53 return val
54 if isinstance(val, bytes):
55 return val.decode()
56 return str(val)
58def tostrlist(address, nitems):
59 return [str(i, 'utf-8') for i in (nitems*c_char_p).from_address(address)]
61def add_dot2path():
62 """add this folder to begninng of PATH environmental variable"""
63 sep = ':'
64 if os.name == 'nt': sep = ';'
65 paths = os.environ.get('PATH','').split(sep)
66 paths.insert(0, os.path.abspath(os.curdir))
67 os.environ['PATH'] = sep.join(paths)
70XDILIB = None
71def get_xdilib():
72 """make initial connection to XDI dll"""
73 global XDILIB
74 if XDILIB is None:
75 XDILIB = get_dll('xdifile')
76 XDILIB.XDI_errorstring.restype = c_char_p
77 return XDILIB
79class XDIFileException(Exception):
80 """XDI File Exception: General Errors"""
81 def __init__(self, msg, **kws):
82 Exception.__init__(self)
83 self.msg = msg
84 def __str__(self):
85 return self.msg
87class XDIFile(object):
88 """ XAS Data Interchange Format:
90 See https://github.com/XraySpectrscopy/XAS-Data-Interchange
92 for further details
94 >>> xdi_file = XDFIile(filename)
96 Principle methods:
97 read(): read XDI data file, set column data and attributes
98 write(filename): write xdi_file data to an XDI file.
99 """
100 _invalid_msg = "invalid data for '%s': was expecting %s, got '%s'"
102 def __init__(self, filename=None, labels=None):
103 self.filename = filename
104 self.xdi_pyversion = __version__
105 # self.xdilib = get_xdilib()
106 self.comments = []
107 self.data = []
108 self.attrs = {}
109 self.status = None
110 self.user_labels = labels
111 if self.filename:
112 self.read(self.filename)
114 def write(self, filename):
115 "write out an XDI File"
116 print( 'Writing XDI file not currently supported')
118 def read(self, filename=None):
119 """read validate and parse an XDI datafile into python structures
120 """
121 if filename is None and self.filename is not None:
122 filename = self.filename
123 pxdi = pointer(XDIFileStruct())
125 xdilib = get_xdilib()
126 self.status = xdilib.XDI_readfile(filename.encode(), pxdi)
127 if self.status < 0:
128 msg = bytes2str(xdilib.XDI_errorstring(self.status))
129 xdilib.XDI_cleanup(pxdi, self.status)
130 msg = 'Error reading XDIFile %s\n%s' % (filename, msg)
131 raise ValueError(msg)
133 xdi = pxdi.contents
134 for attr in dict(xdi._fields_):
135 setattr(self, attr, getattr(xdi, attr))
137 self.array_labels = tostrlist(xdi.array_labels, self.narrays)
139 if self.user_labels is not None:
140 ulab = self.user_labels.replace(',', ' ')
141 ulabs = [l.strip() for l in ulab.split()]
142 self.array_labels[:len(ulabs)] = ulabs
144 arr_units = tostrlist(xdi.array_units, self.narrays)
145 self.array_units = []
146 self.array_addrs = []
147 for unit in arr_units:
148 addr = ''
149 if '||' in unit:
150 unit, addr = [x.strip() for x in unit.split('||', 1)]
151 self.array_units.append(unit)
152 self.array_addrs.append(addr)
154 mfams = tostrlist(xdi.meta_families, self.nmetadata)
155 mkeys = tostrlist(xdi.meta_keywords, self.nmetadata)
156 mvals = tostrlist(xdi.meta_values, self.nmetadata)
157 self.attrs = {}
158 for fam, key, val in zip(mfams, mkeys, mvals):
159 fam = fam.lower()
160 key = key.lower()
161 if fam not in self.attrs:
162 self.attrs[fam] = {}
163 self.attrs[fam][key] = val
165 parrays = (xdi.narrays*c_void_p).from_address(xdi.array)[:]
166 self.data = [(xdi.npts*c_double).from_address(p)[:] for p in parrays]
168 nout = xdi.nouter
169 outer, breaks = [], []
170 if nout > 1:
171 outer = (nout*c_double).from_address(xdi.outer_array)[:]
172 breaks = (nout*c_long).from_address(xdi.outer_breakpts)[:]
173 for attr in ('outer_array', 'outer_breakpts', 'nouter'):
174 delattr(self, attr)
175 self.outer_array = array(outer)
176 self.outer_breakpts = array(breaks)
178 self.data = array(self.data)
179 self.data.shape = (self.narrays, self.npts)
180 self._assign_arrays()
181 for attr in ('nmetadata', 'narray_labels', 'meta_families',
182 'meta_keywords', 'meta_values', 'array'):
183 delattr(self, attr)
184 xdilib.XDI_cleanup(pxdi, 0)
186 def _assign_arrays(self):
187 """assign data arrays for principle data attributes:
188 energy, angle, i0, itrans, ifluor, irefer,
189 mutrans, mufluor, murefer, etc. """
191 xunits = 'eV'
192 xname = None
193 ix = -1
194 self.data = array(self.data)
196 for idx, name in enumerate(self.array_labels):
197 dat = self.data[idx,:]
198 setattr(self, name, dat)
199 if name in ('energy', 'angle'):
200 ix = idx
201 xname = name
202 units = self.array_units[idx]
203 if units is not None:
204 xunits = units
206 # convert energy to angle, or vice versa
207 monodat = {}
208 if 'mono' in self.attrs:
209 monodat = self.attrs['mono']
210 elif 'monochromator' in self.attrs:
211 monodat = self.attrs['monochromator']
213 if ix >= 0 and 'd_spacing' in monodat:
214 dspacing = monodat['d_spacing'].strip()
215 dunits = 'Angstroms'
216 if ' ' in dspacing:
217 dspacing, dunits = dspacing.split(None, 1)
218 self.dspacing = float(dspacing)
219 self.dspacing_units = dunits
221 omega = PLANCK_HC/(2*self.dspacing)
222 if xname == 'energy' and not hasattr(self, 'angle'):
223 energy_ev = self.energy
224 if xunits.lower() == 'kev':
225 energy_ev = 1000. * energy_ev
226 self.angle = RAD2DEG * arcsin(omega/energy_ev)
227 elif xname == 'angle' and not hasattr(self, 'energy'):
228 angle_rad = self.angle
229 if xunits.lower() in ('deg', 'degrees'):
230 angle_rad = angle_rad / RAD2DEG
231 self.energy = omega/sin(angle_rad)
233 if hasattr(self, 'i0'):
234 if hasattr(self, 'itrans') and not hasattr(self, 'mutrans'):
235 self.mutrans = -log(self.itrans / (self.i0+1.e-12))
236 elif hasattr(self, 'mutrans') and not hasattr(self, 'itrans'):
237 self.itrans = self.i0 * exp(-self.mutrans)
238 if hasattr(self, 'ifluor') and not hasattr(self, 'mufluor'):
239 self.mufluor = self.ifluor/(self.i0+1.e-12)
241 elif hasattr(self, 'mufluor') and not hasattr(self, 'ifluor'):
242 self.ifluor = self.i0 * self.mufluor
244 if hasattr(self, 'itrans'):
245 if hasattr(self, 'irefer') and not hasattr(self, 'murefer'):
246 self.murefer = -log(self.irefer / (self.itrans+1.e-12))
248 elif hasattr(self, 'murefer') and not hasattr(self, 'irefer'):
249 self.irefer = self.itrans * exp(-self.murefer)
252def read_xdi(filename, labels=None):
253 """read an XDI File into a Group
255 Arguments:
256 filename (str): name of file to read
257 labels (str or None): string to use for setting array names [None]
259 Returns:
260 Group
262 A data group containing data read from file, with XDI attributes and
263 conventions.
265 Notes:
266 1. See https://github.com/XraySpectrscopy/XAS-Data-Interchange
268 2. if `labels` is `None` (default), the names of the output arrays
269 will be determined from the XDI column label in the XDI header.
270 To override these array names, use a string with space or comma
271 separating names for the arrays.
274 Example:
275 >>> from larch.io import xdi
276 >>> fe3_data = read_xdi('FeXAFS_Fe2O3.001')
277 >>> print(fe3_data.array_labels)
278 ['energy', 'mutrans', 'i0']
280 >>> fec3 = read_xdi('fe3c_rt.xdi', labels='e, x, y')
281 >>> print(fec3.array_labels)
282 ['e', 'x', 'y']
284 See Also:
285 read_ascii
287 """
288 xdif = XDIFile(filename, labels=labels)
289 group = Group()
290 for key, val in xdif.__dict__.items():
291 if not key.startswith('_'):
292 if key in string_attrs:
293 val = tostr(val)
294 setattr(group, key, val)
295 group.__name__ ='XDI file %s' % filename
296 doc = ['%i arrays, %i npts' % (xdif.narrays, xdif.npts)]
297 arr_labels = getattr(xdif, 'array_labels', None)
298 if arr_labels is not None:
299 doc.append("Array Labels: %s" % repr(arr_labels))
300 group.__doc__ = '\n'.join(doc)
302 group.path = filename
303 path, fname = os.path.split(filename)
304 group.filename = fname
305 return group