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

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 

7 

8__version__ = '1.2.0larch' 

9 

10from numpy import array, exp, log, sin, arcsin 

11 

12from .. import Group 

13from ..larchlib import get_dll 

14from ..utils.strutils import bytes2str, str2bytes 

15from ..utils.physical_constants import RAD2DEG, PLANCK_HC 

16 

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)] 

44 

45string_attrs = ('comments', 'edge', 'element', 'error_line', 

46 'error_message', 'extra_version', 'filename', 

47 'outer_label', 'xdi_libversion', 'xdi_pyversion', 

48 'xdi_version') 

49 

50 

51def tostr(val): 

52 if isinstance(val, str): 

53 return val 

54 if isinstance(val, bytes): 

55 return val.decode() 

56 return str(val) 

57 

58def tostrlist(address, nitems): 

59 return [str(i, 'utf-8') for i in (nitems*c_char_p).from_address(address)] 

60 

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) 

68 

69 

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 

78 

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 

86 

87class XDIFile(object): 

88 """ XAS Data Interchange Format: 

89 

90 See https://github.com/XraySpectrscopy/XAS-Data-Interchange 

91 

92 for further details 

93 

94 >>> xdi_file = XDFIile(filename) 

95 

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'" 

101 

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) 

113 

114 def write(self, filename): 

115 "write out an XDI File" 

116 print( 'Writing XDI file not currently supported') 

117 

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()) 

124 

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) 

132 

133 xdi = pxdi.contents 

134 for attr in dict(xdi._fields_): 

135 setattr(self, attr, getattr(xdi, attr)) 

136 

137 self.array_labels = tostrlist(xdi.array_labels, self.narrays) 

138 

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 

143 

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) 

153 

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 

164 

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] 

167 

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) 

177 

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) 

185 

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. """ 

190 

191 xunits = 'eV' 

192 xname = None 

193 ix = -1 

194 self.data = array(self.data) 

195 

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 

205 

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'] 

212 

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 

220 

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) 

232 

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) 

240 

241 elif hasattr(self, 'mufluor') and not hasattr(self, 'ifluor'): 

242 self.ifluor = self.i0 * self.mufluor 

243 

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)) 

247 

248 elif hasattr(self, 'murefer') and not hasattr(self, 'irefer'): 

249 self.irefer = self.itrans * exp(-self.murefer) 

250 

251 

252def read_xdi(filename, labels=None): 

253 """read an XDI File into a Group 

254 

255 Arguments: 

256 filename (str): name of file to read 

257 labels (str or None): string to use for setting array names [None] 

258 

259 Returns: 

260 Group 

261 

262 A data group containing data read from file, with XDI attributes and 

263 conventions. 

264 

265 Notes: 

266 1. See https://github.com/XraySpectrscopy/XAS-Data-Interchange 

267 

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. 

272 

273 

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'] 

279 

280 >>> fec3 = read_xdi('fe3c_rt.xdi', labels='e, x, y') 

281 >>> print(fec3.array_labels) 

282 ['e', 'x', 'y'] 

283 

284 See Also: 

285 read_ascii 

286 

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) 

301 

302 group.path = filename 

303 path, fname = os.path.split(filename) 

304 group.filename = fname 

305 return group