From dc67388c2383a92ff93577fa69d1b3f4684dc7cc Mon Sep 17 00:00:00 2001 From: LightSoar Date: Sat, 2 Feb 2019 23:39:13 -0500 Subject: [PATCH 1/2] extracting format-specific statements to dedicated classes --- convert.py | 0 convert_gui.py | 0 spc/spc.py | 899 +++++++++++++++++---------------- spc/sub.py | 2 +- spc/{global_fun.py => util.py} | 6 +- 5 files changed, 465 insertions(+), 442 deletions(-) mode change 100644 => 100755 convert.py mode change 100644 => 100755 convert_gui.py rename spc/{global_fun.py => util.py} (100%) diff --git a/convert.py b/convert.py old mode 100644 new mode 100755 diff --git a/convert_gui.py b/convert_gui.py old mode 100644 new mode 100755 diff --git a/spc/spc.py b/spc/spc.py index 0baadca..ddafd6d 100644 --- a/spc/spc.py +++ b/spc/spc.py @@ -4,396 +4,44 @@ author: Rohan Isaac """ +# pylint: disable=invalid-name from __future__ import division, absolute_import, unicode_literals, print_function import struct import numpy as np from .sub import subFile, subFileOld -from .global_fun import read_subheader, flag_bits +from .util import read_subheader, flag_bits - -class File: - """ - Starts loading the data from a .SPC spectral file using data from the - header. Stores all the attributes of a spectral file: - - Data - ---- - content: Full raw data - sub[i]: sub file object for each subfileFor each subfile - sub[i].y: y data for each subfile - x: x-data, global, or for the first subheader - - Examples - -------- - >>> import spc - >>> ftir_1 = spc.File('/path/to/ftir.spc') - """ - - # Format strings for various parts of the file - # calculate size of strings using `struct.calcsize(string)` - head_str = "> 20 - self.month = (d >> 16) % (2**4) - self.day = (d >> 11) % (2**5) - self.hour = (d >> 6) % (2**5) - self.minute = d % (2**6) - - # null terminated string, replace null characters with spaces - # split and join to remove multiple spaces - try: - self.cmnt = ' '.join((self.fcmnt.replace('\x00', ' ')).split()) - except: - self.cmnt = self.fcmnt - - # figure out type of file - if self.fnsub > 1: - self.dat_multi = True - - if self.txyxys: - # x values are given - self.dat_fmt = '-xy' - elif self.txvals: - # only one subfile, which contains the x data - self.dat_fmt = 'x-y' - else: - # no x values are given, but they can be generated - self.dat_fmt = 'gx-y' - - print('{}({})'.format(self.dat_fmt, self.fnsub)) - - sub_pos = self.head_siz - - if not self.txyxys: - # txyxys don't have global x data - if self.txvals: - # if global x data is given - x_dat_pos = self.head_siz - x_dat_end = self.head_siz + (4 * self.fnpts) - self.x = np.array( - [struct.unpack_from( - 'f', content[x_dat_pos:x_dat_end], 4 * i)[0] - for i in range(0, self.fnpts)]) - sub_pos = x_dat_end - else: - # otherwise generate them - self.x = np.linspace(self.ffirst, self.flast, num=self.fnpts) - - # make a list of subfiles - self.sub = [] - - # if subfile directory is given - if self.dat_fmt == '-xy' and self.fnpts > 0: - self.directory = True - # loop over entries in directory - for i in range(0, self.fnsub): - ssfposn, ssfsize, ssftime = struct.unpack( - ' fxtype - # oytype -> fytype - self.oftflgs, \ - self.oversn, \ - self.oexp, \ - self.onpts, \ - self.ofirst, \ - self.olast, \ - self.fxtype, \ - self.fytype, \ - self.oyear, \ - self.omonth, \ - self.oday, \ - self.ohour, \ - self.ominute, \ - self.ores, \ - self.opeakpt, \ - self.onscans, \ - self.ospare, \ - self.ocmnt, \ - self.ocatxt, \ - self.osubh1 = struct.unpack(self.old_head_str.encode('utf8'), - content[:self.old_head_siz]) - - # Flag bits (assuming same) - self.tsprec, \ - self.tcgram, \ - self.tmulti, \ - self.trandm, \ - self.tordrd, \ - self.talabs, \ - self.txyxys, \ - self.txvals = flag_bits(self.oftflgs)[::-1] - - # fix data types - self.oexp = int(self.oexp) - self.onpts = int(self.onpts) # can't have floating num of pts - self.ofirst = float(self.ofirst) - self.olast = float(self.olast) - - # Date information - # !! to fix !! - # Year collected (0=no date/time) - MSB 4 bits are Z type - - # extracted as characters, using ord - self.omonth = ord(self.omonth) - self.oday = ord(self.oday) - self.ohour = ord(self.ohour) - self.ominute = ord(self.ominute) - - # number of scans (? subfiles sometimes ?) - self.onscans = int(self.onscans) - - # null terminated strings - self.ores = self.ores.split(b'\x00')[0] - self.ocmnt = self.ocmnt.split(b'\x00')[0] - - # can it have separate x values ? - self.x = np.linspace(self.ofirst, self.olast, num=self.onpts) - - # make a list of subfiles - self.sub = [] - - # already have subheader from main header, retrace steps - sub_pos = self.old_head_siz - self.subhead_siz - - # for each subfile - # in the old format we don't know how many subfiles to expect, - # just looping till we run out - i = 0 - while True: - try: - # read in subheader - subhead_lst = read_subheader(content[sub_pos:sub_pos + self.subhead_siz]) - - if subhead_lst[6] > 0: - # default to subfile points, unless it is zero - pts = subhead_lst[6] - else: - pts = self.onpts - - # figure out size of subheader - dat_siz = (4 * pts) - sub_end = sub_pos + self.subhead_siz + dat_siz - - # read into object, add to list - # send it pts since we have already figured that out - self.sub.append(subFileOld( - content[sub_pos:sub_end], pts, self.oexp, self.txyxys)) - # update next subfile postion, and index - sub_pos = sub_end - - i += 1 - except: - # zero indexed, set the total number of subfile - self.fnsub = i + 1 - break - - # assuming it can't have separate x values - self.dat_fmt = 'gx-y' - print('{}({})'.format(self.dat_fmt, self.fnsub)) - - self.fxtype = ord(self.fxtype) - self.fytype = ord(self.fytype) - # need to find from year apparently - self.fztype = 0 - self.set_labels() - - # -------------------------------------------- - # SHIMADZU - # -------------------------------------------- - elif self.fversn == b'\xcf': - print("Highly experimental format, may not work ") - raw_data = content[10240:] # data starts here (maybe every time) - # spacing between y and x data is atleast 0 bytes - s_32 = chr(int('0', 2)) * 32 - s_8 = chr(int('0', 2)) * 8 # zero double - dat_len = raw_data.find(s_32) - for i in range(dat_len, len(raw_data), 8): - # find first non zero double - if raw_data[i:i + 8] != s_8: - break - dat_siz = int(dat_len / 8) - self.y = struct.unpack(('<' + dat_siz * 'd').encode('utf8'), raw_data[:dat_len]) - self.x = struct.unpack(('<' + dat_siz * 'd').encode('utf8'), raw_data[i:i + dat_len]) + def unpack_flag_bits(self): + # Flag bits (assuming same) + self.tsprec, \ + self.tcgram, \ + self.tmulti, \ + self.trandm, \ + self.tordrd, \ + self.talabs, \ + self.txyxys, \ + self.txvals = flag_bits(self.tflgs)[::-1] + if self.txyxys: + # x values are given + self.dat_fmt = '-xy' + elif self.txvals: + # only one subfile, which contains the x data + self.dat_fmt = 'x-y' else: - print("File type %s not supported yet. Please add issue. " - % hex(ord(self.fversn))) - self.content = content - - # ------------------------------------------------------------------------ - # Process other data - # ------------------------------------------------------------------------ + # no x values are given, but they can be generated + self.dat_fmt = 'gx-y' def set_labels(self): """ @@ -434,13 +82,13 @@ def set_labels(self): "Millimeters (mm)", "Hours"] - if self.fxtype < 30: - self.xlabel = fxtype_op[self.fxtype] + if self.xtype < 30: + self.xlabel = fxtype_op[self.xtype] else: self.xlabel = "Unknown" - if self.fztype < 30: - self.zlabel = fxtype_op[self.fztype] + if self.ztype < 30: + self.zlabel = fxtype_op[self.ztype] else: self.zlabel = "Unknown" @@ -481,10 +129,10 @@ def set_labels(self): "Arbitrary or Single Beam with Valley Peaks", "Emission"] - if self.fytype < 27: - self.ylabel = fytype_op[self.fytype] - elif self.fytype > 127 and self.fytype < 132: - self.ylabel = fytype_op2[self.fytype - 128] + if self.ytype < 27: + self.ylabel = fytype_op[self.ytype] + elif 127 < self.ytype < 132: + self.ylabel = fytype_op2[self.ytype - 128] else: self.ylabel = "Unknown" @@ -495,7 +143,7 @@ def set_labels(self): # split it based on 00 string # format x, y, z if self.talabs: - ll = self.fcatxt.split(b'\x00') + ll = self.catxt.split(b'\x00') if len(ll) > 2: # make sure there are enough items to extract from xl, yl, zl = ll[:3] @@ -526,7 +174,382 @@ def set_exp_type(self): "Atomic Spectrum", "Chromatography Diode Array Spectra"] - self.exp_type = fexper_op[self.fexper] + self.exp_type = fexper_op[self.exper] + + +class OldFormat(FileFormat): + # Format string for the header + # Calculate size of strings using `struct.calcsize(string)` + head_str = " 0: + # default to subfile points, unless it is zero + pts = subhead_lst[6] + else: + pts = self.npts + + # figure out size of subheader + dat_siz = (4 * pts) + sub_end = sub_pos + self.subhead_siz + dat_siz + + # read into object, add to list + # send it pts since we have already figured that out + self.sub.append(subFileOld( + content[sub_pos:sub_end], pts, self.exp, self.txyxys)) + # update next subfile postion, and index + sub_pos = sub_end + + i += 1 + except: + # zero indexed, set the total number of subfile + self.nsub = i + 1 + break + + print('{}({})'.format(self.dat_fmt, self.nsub)) + + self.set_labels() + + def unpack_header(self, content): + self.tflgs, \ + self.versn, \ + self.exp, \ + self.npts, \ + self.first, \ + self.last, \ + self.xtype, \ + self.ytype, \ + self.year, \ + self.month, \ + self.day, \ + self.hour, \ + self.minute, \ + self.res, \ + self.peakpt, \ + self.nscans, \ + self.spare, \ + self.cmnt, \ + self.catxt, \ + self.subh1 = struct.unpack(self.head_str.encode('utf8'), + content[:self.head_siz]) + + # fix data types + self.exp = int(self.exp) + self.npts = int(self.npts) # can't have floating num of pts + self.first = float(self.first) + self.last = float(self.last) + + # Date information + # !! to fix !! + # Year collected (0=no date/time) - MSB 4 bits are Z type + + # extracted as characters, using ord + self.month = ord(self.month) + self.day = ord(self.day) + self.hour = ord(self.hour) + self.minute = ord(self.minute) + + # number of scans (? subfiles sometimes ?) + self.nscans = int(self.nscans) + + # null terminated strings + self.res = self.res.split(b'\x00')[0] + self.cmnt = self.cmnt.split(b'\x00')[0] + + self.xtype = ord(self.xtype) + self.ytype = ord(self.ytype) + # need to find from year apparently + self.ztype = 0 + +class NewFormat(FileFormat): + # Format string for the header + # Calculate size of strings using `struct.calcsize(string)` + head_str = " 0: + self.directory = True + # loop over entries in directory + for i in range(0, self.nsub): + ssfposn, ssfsize, ssftime = struct.unpack( + '> 20 + self.month = (d >> 16) % (2**4) + self.day = (d >> 11) % (2**5) + self.hour = (d >> 6) % (2**5) + self.minute = d % (2**6) + + # null terminated string, replace null characters with spaces + # split and join to remove multiple spaces + try: + self.cmnt = ' '.join((self.cmnt.replace('\x00', ' ')).split()) + except: + pass + + # figure out type of file + self.dat_multi = self.nsub > 1 + +class NewFormatMSB(NewFormat): + def __init__(self, content): + print("New MSB 1st, yet to be implemented") + pass # To be implemented + +class ShimadzuFormat(FileFormat): + def __init__(self, content): + print("Highly experimental format, may not work ") + raw_data = content[10240:] # data starts here (maybe every time) + # spacing between y and x data is atleast 0 bytes + s_32 = chr(int('0', 2)) * 32 + s_8 = chr(int('0', 2)) * 8 # zero double + dat_len = raw_data.find(s_32) + for i in range(dat_len, len(raw_data), 8): + # find first non zero double + if raw_data[i:i + 8] != s_8: + break + dat_siz = int(dat_len / 8) + self.y = struct.unpack(('<' + dat_siz * 'd').encode('utf8'), raw_data[:dat_len]) + self.x = struct.unpack(('<' + dat_siz * 'd').encode('utf8'), raw_data[i:i + dat_len]) + +class File: + """ + Starts loading the data from a .SPC spectral file using data from the + header. Stores all the attributes of a spectral file: + + Data + ---- + content: Full raw data + sub[i]: sub file object for each subfileFor each subfile + sub[i].y: y data for each subfile + x: x-data, global, or for the first subheader + + Examples + -------- + >>> import spc + >>> ftir_1 = spc.File('/path/to/ftir.spc') + """ + + # ------------------------------------------------------------------------ + # CONSTRUCTOR + # ------------------------------------------------------------------------ + + def __init__(self, filename): + # load entire into memory temporarly + with open(filename, "rb") as fin: + content = fin.read() + # print "Read raw data" + + self.length = len(content) + # extract first two bytes to determine file type version + # TODO remove self.tflg + self.tflg, self.versn = struct.unpack(' Date: Sun, 3 Feb 2019 02:07:25 -0500 Subject: [PATCH 2/2] correcting code to work with SHimadzu UV-Vis and Renishaw Raman --- spc/spc.py | 19 +- spc/sub.py | 23 +- test_data/Renishaw Ramascope Raman.spc | Bin 0 -> 1301 bytes test_data/Shimadzu UV-Vis.spc | Bin 0 -> 27136 bytes .../txt2/Renishaw Ramascope Raman.spc.txt | 43 + test_data/txt2/Shimadzu UV-Vis.spc.txt | 1001 +++++++++++++++++ 6 files changed, 1072 insertions(+), 14 deletions(-) create mode 100644 test_data/Renishaw Ramascope Raman.spc create mode 100644 test_data/Shimadzu UV-Vis.spc create mode 100644 test_data/txt2/Renishaw Ramascope Raman.spc.txt create mode 100644 test_data/txt2/Shimadzu UV-Vis.spc.txt diff --git a/spc/spc.py b/spc/spc.py index ddafd6d..ad185d3 100644 --- a/spc/spc.py +++ b/spc/spc.py @@ -10,7 +10,7 @@ import struct import numpy as np -from .sub import subFile, subFileOld +from .sub import subFile, subFileOld, subFileShimadzu from .util import read_subheader, flag_bits class FileFormat: @@ -303,7 +303,7 @@ def __init__(self, content): self.unpack_header(content) self.unpack_flag_bits() - # TODO use __repr__ instead? + # TODO use __str__ instead? print('{}({})'.format(self.dat_fmt, self.nsub)) sub_pos = self.head_siz @@ -483,9 +483,20 @@ def __init__(self, content): if raw_data[i:i + 8] != s_8: break dat_siz = int(dat_len / 8) + + self.dat_fmt = 'x-y' + self.nsub = 1 + self.y = struct.unpack(('<' + dat_siz * 'd').encode('utf8'), raw_data[:dat_len]) self.x = struct.unpack(('<' + dat_siz * 'd').encode('utf8'), raw_data[i:i + dat_len]) + self.ylabel = '' + self.xlabel = '' + + # creating a `sub` member to maintain consistency + self.sub = [] + self.sub.append(subFileShimadzu(self.y)) + class File: """ Starts loading the data from a .SPC spectral file using data from the @@ -516,7 +527,6 @@ def __init__(self, filename): self.length = len(content) # extract first two bytes to determine file type version - # TODO remove self.tflg self.tflg, self.versn = struct.unpack('*iY9?*G~F^cH@7$I?anfLd&xC2 zaGp$Ptgr~!?rl}!p!EKH|@jh$5r1!wp6AU1(?8Q7WkzV9*LH*aU)EOZ_N zHJ*%r+uJ@j9Irn=)rZ>@l-<@2KN`EP#B?$)vFW_t-PcMtTJ?>{Ex@g`$0j5|TYOS^ zBF;85Ry!UqZ>D~$)-H4=&SdL6$z51)_RiZ143*1r>Af%S@2<$-GbMO-$dUOsF5De) za+3T4(UGzbJP!7)-8ESbRv~`vEEk zwqu$3TZE3+MR;+8%02H&@^g_&<5x*GzBq9E>t&dnB69H_1F=?+PoKLmwnX97OaX3v zaNxn53;S+T*)>SuV~(7O|LIPE;r0GE5$gjzn%<-LBi%6j41{KprM^+_*Qf{?_Yn?) z3Zq4I#GzGzJ?wYm0y&z^#P7mRi>!;yViT#ClFgj2hgIs?)p|g!W~>NJ(-cDm!xh_) zyaE;0@jk=Qvzc=^R7oBPQu+CShmoLmm3h>o8dsBvPBW*ha2|S$ z*aFvWK|Nl>6{`s$1)&r&(Nw9Fw#W|nT&qziiIx807FpsX3Y%R-No*obkz;iq$1>A2 P43s}=r2uLHsRH~1Z_x>H literal 0 HcmV?d00001 diff --git a/test_data/Shimadzu UV-Vis.spc b/test_data/Shimadzu UV-Vis.spc new file mode 100644 index 0000000000000000000000000000000000000000..facc7cd4eb2164c9f39a39e51367a08c27984b9c GIT binary patch literal 27136 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