├── MANIFEST.in ├── .gitignore ├── setup.py ├── LICENSE ├── README.rst └── lecroyparser └── __init__.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules and backups 2 | *.pyc 3 | *.py~ 4 | 5 | # Setuptools distribution folder. 6 | /dist/ 7 | 8 | # Python egg metadata, regenerated from source files by setuptools. 9 | /*.egg-info 10 | 11 | # .rst backups 12 | *.rst~ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('README.rst') as f: 4 | long_description = f.read() 5 | 6 | setup(name='lecroyparser', 7 | version='1.4.4', 8 | description='Parse LeCroy Binary Files.', 9 | long_description=long_description, 10 | long_description_content_type='text/markdown', 11 | classifiers=['License :: OSI Approved :: MIT License', 12 | 'Programming Language :: Python', 13 | 'Topic :: Scientific/Engineering :: Physics'], 14 | keywords='LeCroy Binary Scope Parse', 15 | url='http://github.com/bennomeier/lecroyparser', 16 | author='Benno Meier', 17 | author_email='meier.benno@gmail.com', 18 | license='MIT', 19 | packages=['lecroyparser'], 20 | include_package_data=True, 21 | install_requires = [ 22 | 'numpy', 23 | ], 24 | zip_safe=False) 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Benno Meier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | lecroyparser 2 | ============ 3 | 4 | 5 | leCroyParser.py parses binary files as written by LeCroy scopes. 6 | 7 | leCroyParser.py is derived from the matlab program 8 | ReadLeCroyBinaryWaveform.m, available at 9 | , 10 | Original version (c)2001 Hochschule fr Technik+Architektur Luzern 11 | Fachstelle Elektronik 6048 Horw, Switzerland Slightly modified by Alan 12 | Blankman, LeCroy Corporation, 2006 13 | 14 | Further elements for the code were taken from pylecroy, written by Steve Bian 15 | 16 | A useful resource for modifications is the LeCroy Remote Control Manual 17 | available at 18 | 19 | 20 | Lecroyparser has been tested with Python 2.7 and Python 3.6, 3.8 21 | 22 | Installation 23 | ------------ 24 | 25 | lecroyparser is available at pip. It may be installed 26 | with 27 | 28 | >>> pip install lecroyparser 29 | 30 | or with 31 | 32 | 33 | >>> easy_install lecroyparser 34 | 35 | Usage 36 | ----- 37 | 38 | To import a single trace, instantiate a ScopeData object by passing it a 39 | path, i.e. 40 | 41 | >>> import lecroyparser 42 | >>> path = "/home/benno/Dropbox/RESEARCH/bullet/experiments/scopeTraces/201804/C1180421_typicalShot00000.trc" 43 | >>> data = lecroyparser.ScopeData(path) 44 | 45 | 46 | The x and y data are stored as numpy arrays in data.x and data.y 47 | 48 | Alternatively, to parse several channels set the optional keyword 49 | argument parseAll to True, i.e. 50 | 51 | >>> data = lecroyparser.ScopeData(path, parseAll = True) 52 | 53 | This will parse all files in the specified folder with a matching 54 | filename. I.e., if the provided path is as above, then the files 55 | 56 | .. code-block:: console 57 | 58 | C2180421_typicalShot00000.trc 59 | C3180421_typicalShot00000.trc 60 | C4180421_typicalShot00000.trc 61 | 62 | 63 | will pe parsed as well. 64 | 65 | Next to reading files, it is possible to read a binary buffer/string directly. For this, supply the data argument instead of file: 66 | 67 | >>> import lecroyparser 68 | >>> path = "/home/benno/Dropbox/RESEARCH/bullet/experiments/scopeTraces/201804/C1180421_typicalShot00000.trc" 69 | >>> contents = open(path, 'rb').read() 70 | >>> data = lecroyparser.ScopeData(data=contents) 71 | 72 | This is especially useful when reading data directly from oscilloscope using python-vx11 or similar software. 73 | 74 | Additionally, it is possible to limit the number of samples in the output array, by overwriting the sparse keyword: 75 | 76 | >>> data = lecroyparser.ScopeData(path, parseAll = True, sparse = 1000) 77 | 78 | will limit the samples in the x and y dimensions to 1000. 79 | 80 | Information about the file can be obtained by calling print(data) 81 | 82 | .. code-block:: console 83 | 84 | >>> print(data) 85 | 86 | Le Croy Scope Data 87 | Path: /Users/... 88 | Endianness: < 89 | Instrument: LECROYHDO4104 90 | Instrunemt Number: 19359 91 | Template Name: LECROY\_2\_3 92 | Channel: Channel 4 93 | Vertical Coupling: DC1M 94 | Bandwidth Limit: on 95 | Record Type: single\_sweep 96 | Processing: No Processing >>> TimeBase: 200 ms/div 97 | TriggerTime: 2018-04-21 11:50:45.76 98 | 99 | 100 | 101 | License 102 | ------- 103 | 104 | MIT License 105 | 106 | Copyright (c) 2018-2021 Benno Meier, Jeroen van Oorschot 107 | 108 | Permission is hereby granted, free of charge, to any person obtaining a 109 | copy of this software and associated documentation files (the 110 | "Software"), to deal in the Software without restriction, including 111 | without limitation the rights to use, copy, modify, merge, publish, 112 | distribute, sublicense, and/or sell copies of the Software, and to 113 | permit persons to whom the Software is furnished to do so, subject to 114 | the following conditions: 115 | 116 | The above copyright notice and this permission notice shall be included 117 | in all copies or substantial portions of the Software. 118 | 119 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 120 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 121 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 122 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 123 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 124 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 125 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 126 | -------------------------------------------------------------------------------- /lecroyparser/__init__.py: -------------------------------------------------------------------------------- 1 | """ leCroyParser.py 2 | (c) Benno Meier, 2018 published under an MIT license. 3 | 4 | leCroyParser.py is derived from the matlab programme ReadLeCroyBinaryWaveform.m, 5 | which is available at Matlab Central. 6 | a useful resource for modifications is the LeCroy Remote Control Manual 7 | available at http://cdn.teledynelecroy.com/files/manuals/dda-rcm-e10.pdf 8 | ------------------------------------------------------ 9 | Original version (c)2001 Hochschule fr Technik+Architektur Luzern 10 | Fachstelle Elektronik 11 | 6048 Horw, Switzerland 12 | Slightly modified by Alan Blankman, LeCroy Corporation, 2006 13 | 14 | Further elements for the code were taken from pylecroy, written by Steve Bian 15 | 16 | lecroyparser defines the ScopeData object. 17 | Tested in Python 2.7 and Python 3.6 18 | 19 | Updated 2020 Jeroen van Oorschot, Eindhoven University of Technology 20 | """ 21 | 22 | import sys 23 | import numpy as np 24 | import glob 25 | 26 | 27 | class ScopeData(object): 28 | def __init__(self, path=None, data=None, parseAll=False, sparse=-1, secondDigits = 3): 29 | """Import Scopedata as stored under path. 30 | 31 | If parseAll is set to true, search for all files 32 | in the folder commencing with C1...Cx and store the y data in a list. 33 | 34 | If a positive value is provided for sparse, then only #sparse elements will 35 | be stored in x and y. These will be sampled evenly from all data points in 36 | the source file. This can speed up data processing and plotting.""" 37 | 38 | if path: 39 | if data: 40 | raise Exception('Both data and path supplied. Choose either one.') 41 | 42 | self.path = path 43 | 44 | if parseAll: 45 | path=path.replace('\\', '/') 46 | basePath = "/".join(path.split("/")[:-1]) 47 | core_filename = path.split("/")[-1][2:] 48 | 49 | files = sorted(list(glob.iglob(basePath + "/C*" + core_filename))) 50 | 51 | self.y = [] 52 | for f in files: 53 | x, y = self.parseFile(f, sparse=sparse) 54 | self.x = x 55 | self.y.append(y) 56 | 57 | else: 58 | x, y = self.parseFile(path, sparse=sparse) 59 | self.x = x 60 | self.y = y 61 | elif data: 62 | if path: 63 | raise Exception('Both data and path supplied. Choose either one.') 64 | if parseAll: 65 | raise Exception('parseAll option is not available using data input') 66 | 67 | assert type(data) == bytes, 'Please supply data as bytes' 68 | self.path = 'None - from bytes data' # not reading from a path 69 | self.x, self.y = self.parseData(data, sparse, secondDigits = secondDigits) 70 | 71 | def parseFile(self, path, sparse=-1, secondDigits = 3): 72 | self.file = open(path, mode='rb') 73 | 74 | fileContent = self.file.read() 75 | 76 | self.file.close() 77 | del self.file 78 | return self.parseData(data=fileContent, sparse=sparse, secondDigits = secondDigits) 79 | 80 | def parseData(self, data, sparse, secondDigits = 3): 81 | self.data = data 82 | 83 | self.endianness = "<" 84 | 85 | waveSourceList = ["Channel 1", "Channel 2", "Channel 3", "Channel 4", "Unknown"] 86 | verticalCouplingList = ["DC50", "GND", "DC1M", "GND", "AC1M"] 87 | bandwidthLimitList = ["off", "on"] 88 | recordTypeList = ["single_sweep", "interleaved", "histogram", "graph", 89 | "filter_coefficient", "complex", "extrema", "sequence_obsolete", 90 | "centered_RIS", "peak_detect"] 91 | processingList = ["No Processing", "FIR Filter", "interpolated", "sparsed", 92 | "autoscaled", "no_resulst", "rolling", "cumulative"] 93 | 94 | # convert the first 50 bytes to a string to find position of substring WAVEDESC 95 | self.posWAVEDESC = self.data[:50].decode("ascii", "replace").index("WAVEDESC") 96 | 97 | self.commOrder = self.parseInt16(34) # big endian (>) if 0, else little 98 | self.endianness = [">", "<"][self.commOrder] 99 | 100 | self.templateName = self.parseString(16) 101 | self.commType = self.parseInt16(32) # encodes whether data is stored as 8 or 16bit 102 | 103 | self.waveDescriptor = self.parseInt32(36) 104 | self.userText = self.parseInt32(40) 105 | self.trigTimeArray = self.parseInt32(48) 106 | self.waveArray1 = self.parseInt32(60) 107 | 108 | self.instrumentName = self.parseString(76) 109 | self.instrumentNumber = self.parseInt32(92) 110 | 111 | self.traceLabel = "NOT PARSED" 112 | self.waveArrayCount = self.parseInt32(116) 113 | 114 | self.verticalGain = self.parseFloat(156) 115 | self.verticalOffset = self.parseFloat(160) 116 | 117 | self.nominalBits = self.parseInt16(172) 118 | 119 | self.horizInterval = self.parseFloat(176) 120 | self.horizOffset = self.parseDouble(180) 121 | 122 | self.vertUnit = "NOT PARSED" 123 | self.horUnit = "NOT PARSED" 124 | 125 | self.sequenceSegments=self.parseInt32(144) 126 | 127 | self.triggerTime = self.parseTimeStamp(296, secondDigits = secondDigits) 128 | self.recordType = recordTypeList[self.parseInt16(316)] 129 | self.processingDone = processingList[self.parseInt16(318)] 130 | self.timeBase = self.parseTimeBase(324) 131 | self.verticalCoupling = verticalCouplingList[self.parseInt16(326)] 132 | self.bandwidthLimit = bandwidthLimitList[self.parseInt16(334)] 133 | self.waveSource = waveSourceList[self.parseInt16(344)] 134 | 135 | start = self.posWAVEDESC + self.waveDescriptor + self.userText + self.trigTimeArray 136 | if self.commType == 0: # data is stored in 8bit integers 137 | y = np.frombuffer(self.data[start:start + self.waveArray1], dtype=np.dtype((self.endianness + "i1", self.waveArray1)), count=1)[0] 138 | else: # 16 bit integers 139 | length = self.waveArray1 // 2 140 | y = np.frombuffer(self.data[start:start + self.waveArray1], dtype=np.dtype((self.endianness + "i2", length)), count=1)[0] 141 | 142 | # now scale the ADC values 143 | y = self.verticalGain * np.array(y) - self.verticalOffset 144 | 145 | x = np.linspace(0, self.waveArrayCount * self.horizInterval, 146 | num=self.waveArrayCount) + self.horizOffset 147 | 148 | if sparse > 0: 149 | indices = int(len(x) / sparse) * np.arange(sparse) 150 | 151 | x = x[indices] 152 | y = y[indices] 153 | 154 | return x, y 155 | 156 | def unpack(self, pos, formatSpecifier, length): 157 | """ a wrapper that reads binary data 158 | in a given position in the file, with correct endianness, and returns the parsed 159 | data as a tuple, according to the format specifier. """ 160 | start = pos + self.posWAVEDESC 161 | x = np.frombuffer(self.data[start:start + length], self.endianness + formatSpecifier, count=1)[0] 162 | return x 163 | 164 | def parseString(self, pos, length=16): 165 | s = self.unpack(pos, "S{}".format(length), length) 166 | if sys.version_info > (3, 0): 167 | s = s.decode('ascii') 168 | return s 169 | 170 | def parseInt16(self, pos): 171 | return self.unpack(pos, "u2", 2) 172 | 173 | def parseWord(self, pos): 174 | return self.unpack(pos, "i2", 2) 175 | 176 | def parseInt32(self, pos): 177 | return self.unpack(pos, "i4", 4) 178 | 179 | def parseFloat(self, pos): 180 | return self.unpack(pos, "f4", 4) 181 | 182 | def parseDouble(self, pos): 183 | return self.unpack(pos, "f8", 8) 184 | 185 | def parseByte(self, pos): 186 | return self.unpack(pos, "u1", 1) 187 | 188 | def parseTimeStamp(self, pos, secondDigits = 3): 189 | second = self.parseDouble(pos) 190 | minute = self.parseByte(pos + 8) 191 | hour = self.parseByte(pos + 9) 192 | day = self.parseByte(pos + 10) 193 | month = self.parseByte(pos + 11) 194 | year = self.parseWord(pos + 12) 195 | 196 | secondFormat = "{:0" + str(secondDigits + 3) + "." + str(secondDigits) + "f}" 197 | fullFormat = "{}-{:02d}-{:02d} {:02d}:{:02d}:" + secondFormat 198 | 199 | return fullFormat.format(year, month, day, hour, minute, second) 200 | 201 | def parseTimeBase(self, pos): 202 | """ time base is an integer, and encodes timing information as follows: 203 | 0 : 1 ps / div 204 | 1: 2 ps / div 205 | 2: 5 ps/div, up to 47 = 5 ks / div. 100 for external clock""" 206 | 207 | timeBaseNumber = self.parseInt16(pos) 208 | 209 | if timeBaseNumber < 48: 210 | unit = "pnum k"[int(timeBaseNumber / 9)] 211 | value = [1, 2, 5, 10, 20, 50, 100, 200, 500][timeBaseNumber % 9] 212 | return "{} ".format(value) + unit.strip() + "s/div" 213 | elif timeBaseNumber == 100: 214 | return "EXTERNAL" 215 | 216 | def __repr__(self): 217 | string = "Le Croy Scope Data\n" 218 | string += "Path: " + self.path + "\n" 219 | string += "Endianness: " + self.endianness + "\n" 220 | string += "Instrument: " + self.instrumentName + "\n" 221 | string += "Instrument Number: " + str(self.instrumentNumber) + "\n" 222 | string += "Template Name: " + self.templateName + "\n" 223 | string += "Channel: " + self.waveSource + "\n" 224 | string += "WaveArrayCount: " + str(self.waveArrayCount) + "\n" 225 | string += "Vertical Coupling: " + self.verticalCoupling + "\n" 226 | string += "Bandwidth Limit: " + self.bandwidthLimit + "\n" 227 | string += "Record Type: " + self.recordType + "\n" 228 | string += "Processing: " + self.processingDone + "\n" 229 | string += "TimeBase: " + self.timeBase + "\n" 230 | string += "TriggerTime: " + self.triggerTime + "\n" 231 | 232 | return string 233 | 234 | 235 | if __name__ == "__main__": 236 | data = ScopeData(path, parseAll=True) 237 | --------------------------------------------------------------------------------