diff options
-rw-r--r-- | README | 68 | ||||
-rw-r--r-- | __init__.py | 1 | ||||
-rwxr-xr-x | config.py | 2 | ||||
-rwxr-xr-x | draw.py | 57 | ||||
-rwxr-xr-x | fetch.py | 115 | ||||
-rw-r--r-- | lecroy.py | 23 | ||||
-rw-r--r-- | setup.py | 1 | ||||
-rwxr-xr-x | sock.py | 103 | ||||
-rwxr-xr-x | view.py | 2 |
9 files changed, 302 insertions, 70 deletions
@@ -1,14 +1,35 @@ -LeCrunch version 1.0 +LeCrunch version 1.1 ==================== Installation ------------ -You must have h5py, a general-purpose Python interface to the Hierarchical -Data Format Library version 5 installed. The source can be downloaded from -<code.google.com/p/h5py>, or on an ubuntu distribution: +LeCrunch requires a python version >= 2.6 and the following python packages: -~$ sudo apt-get install python-h5py + numpy + The fundamental package needed for scientific computing with python. + On an ubuntu distribution: + + ~$ sudo apt-get install python-numpy + + h5py + A general-purpose Python interface to the Hierarchical Data Format + Library. The source can be downloaded from <code.google.com/p/h5py>, + or on an ubuntu distribution: + + ~$ sudo apt-get install python-h5py + + matplotlib + A python 2D plotting library which produces publication quality figures + in a variety of hardcopy formats and interactive environments across + platforms; on an ubuntu distribution: + + ~$ sudo apt-get install python-matplotlib + +After downloading LeCrunch, you should first edit the file "config.py" and +set the variable "scope_ip" to the ip address of your oscilloscope. If you're +unsure of the ip address of your oscilloscope, just open up a web browser +on the oscilloscope and go to www.whatsmyip.org. Then just source the env.sh file in this directory, and you're good to go! @@ -28,6 +49,43 @@ The syntax to fetch waveforms: where <filename> is the name of the file you would like LeCrunch to store the extracted waveforms; waveforms are stored in the hdf5 file format. +Once you have fetched some waveforms you can plot an overlay with draw.py: + +~$ draw.py <filename> + +You can load and save the scope's configuration with config.py. Suppose you +took some data a month ago and saved it to the file "run001.hdf5". Then, to +restore the scope configuration as it was when you took that data, just run: + +~$ config.py load run001.hdf5 + +In order to send commands directly to the oscilloscope you can run the file +"sock.py" as a script: + +~$ sock.py "display?" +'DISP ON\n' + +~$ sock.py "sequence?" +'SEQ OFF,1000,25E+3 SAMPLE\n' + +~$ sock.py "seq on, 100" + +For a list of commands see the LeCroy Remote Control Manual. If you don't have +a copy, just google it. + +How to Transfer Waveforms at High Speed +--------------------------------------- + +Turn on sequence mode! + +~$ sock.py 'seq on, 1000' + +Be aware that as you turn up the number of segments the scope will take +longer to process a single acquisition. The default timeout value for the +socket connection in sock.py is set to 2 seconds; therefore, if an acquisition +takes longer than 2 seconds, the socket object will raise a timeout exception +when you request a waveform. + Documentation ------------- diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 80ccd7c..0000000 --- a/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from fetch import fetch @@ -40,7 +40,7 @@ Examples: """ commands = ['TIME_DIV', 'COMM_FORMAT', 'COMM_HEADER', 'COMM_ORDER'] + \ - ['TRIG_DELAY', 'TRIG_SELECT', 'TRIG_MODE', 'TRIG_PATTERN'] + \ + ['TRIG_DELAY', 'TRIG_SELECT', 'TRIG_MODE', 'TRIG_PATTERN', 'SEQUENCE'] + \ ['C%i:COUPLING' % i for i in range(1,5)] + \ ['C%i:VOLT_DIV' % i for i in range(1,5)] + \ ['C%i:OFFSET' % i for i in range(1,5)] + \ @@ -0,0 +1,57 @@ +#!/usr/bin/env python +import h5py +import numpy as np +import matplotlib.pyplot as plt +from itertools import cycle, islice + +def roundrobin(*iterables): + pending = len(iterables) + nexts = cycle(iter(it).next for it in iterables) + + while pending: + try: + for next in nexts: + yield next() + except StopIteration: + pending -= 1 + nexts = cycle(islice(nexts, pending)) + +def draw(filename, nevents): + f = h5py.File(filename) + + fig = plt.figure() + for i, dataset in enumerate((f[name] for name in f)): + plt.subplot(len(f), 1, i+1) + nevents = min(options.nevents, len(dataset)) + nsamples = dataset.attrs['wave_array_count']//dataset.attrs['nom_subarray_count'] + dx = dataset.attrs['horiz_interval'] + dy = dataset.attrs['vertical_gain'] + xoffset = dataset.attrs['horiz_offset'] + yoffset = dataset.attrs['vertical_offset'] + + x = np.tile(np.linspace(xoffset, xoffset+dx*nsamples, nsamples)*1e9, (nevents,1)) + y = dataset[:nevents]*dy - yoffset + + plt.plot(*roundrobin(x, y), color='black') + plt.xlabel('Time (ns)') + plt.ylabel('Voltage (V)') + plt.title(dataset.name) + + return fig + +if __name__ == '__main__': + import sys + import optparse + + parser = optparse.OptionParser('%prog file [...]') + parser.add_option('-n', type='int', dest='nevents', default=100) + options, args = parser.parse_args() + + if len(args) < 1: + sys.exit(parser.format_help()) + + figures = [] + for filename in args: + figures.append(draw(filename, options.nevents)) + + plt.show() @@ -21,21 +21,21 @@ import time import socket import struct import h5py +import setup from lecroy import LeCroyScope from config import get_settings -def fetch(filename, events): +def fetch(filename, nevents): """ Fetch and save waveform traces from the oscilloscope. Args: - filename: str Filename to store traces in (in hdf5 format). - - events: int + - nevents: int Number of triggered events to save in `filename`. """ - scope = LeCroyScope('scope01.hep.upenn.edu') - scope.connect() + scope = LeCroyScope(setup.scope_ip, timeout=20.0) # turn off the display scope.send('display off') @@ -52,33 +52,38 @@ def fetch(filename, events): # get wave descriptors for each channel # important to do this before queue is primed! + # need to trigger in order to get correct wave_array_count + scope.trigger() + + time.sleep(5.0) + wavedesc = {} for channel in channels: wavedesc[channel] = scope.getwavedesc(channel) - # prime the output queue - for i in range(10): - scope.trigger() - for channel in channels: - scope.send('c%i:wf? dat1' % channel) - # open up the output file f = h5py.File(filename, 'w') + # set scope configuration for command, setting in settings.items(): f.attrs[command] = setting - dataset = {} + + if 'ON' in f.attrs['SEQUENCE']: + sequence_count = int(f.attrs['SEQUENCE'].split(',')[1]) + + if sequence_count < 1: + raise Exception('sequence count must be a positive number.') + else: + sequence_count = 1 + for channel in channels: - samples = wavedesc[channel]['wave_array_count'] - chunks = (max(1,min(100, events//100)), samples) - dataset[channel] = f.create_dataset('channel%i' % channel, - (events, samples), - dtype=wavedesc[channel]['dtype'], - chunks=chunks, - compression='gzip') + nsamples = wavedesc[channel]['wave_array_count']//sequence_count + + f.create_dataset('channel%i' % channel, (nevents, nsamples), dtype=wavedesc[channel]['dtype'], chunks=(max(1,min(100, nevents//100)), nsamples), compression='gzip') + for key, value in wavedesc[channel].items(): try: - dataset[channel].attrs[key] = value + f['channel%i' % channel].attrs[key] = value except ValueError: pass @@ -86,53 +91,68 @@ def fetch(filename, events): time0 = time.time() try: - for i in range(events): - print '\rSaving event: %i' % (i+1), + i = 0 + while True: + print '\rsaving event: %i' % i, sys.stdout.flush() try: scope.trigger() for channel in channels: - dataset[channel][i] = scope.getwaveform(channel, - wavedesc[channel]) - except (socket.error, struct.error, RuntimeError, OverflowError): - print "\nFailed to receive waveform %i" % (i+1) - # clear the output queue + wave_array = scope.getwaveform(channel, wavedesc[channel]) + + if sequence_count > 1: + try: + f['channel%i' % channel][i:i+sequence_count] = \ + wave_array.reshape(sequence_count, wave_array.size//sequence_count) + except ValueError: + f['channel%i' % channel][i:i+sequence_count] = \ + wave_array.reshape(sequence_count, wave_array.size//sequence_count)[:len(f['channel%i' % channel])-i] + else: + f['channel%i' % channel][i] = wave_array + + except (socket.error, struct.error) as e: + print '\n' + str(e) scope.clear() - # reprime the queue - for i in range(10): - scope.trigger() - for channel in channels: - scope.send('c%i:wf? dat1' % channel) continue + + i += sequence_count + + if i >= nevents: + print '\rsaving event: %i' % i, + break + print + except KeyboardInterrupt: - print - print 'Resizing datasets...' + print '\nresizing datasets...' + for channel in channels: - dataset[channel].resize((i, wavedesc[channel]['wave_array_count'])) - raise KeyboardInterrupt + f['channel%i' % channel].resize((i, wavedesc[channel]['wave_array_count']//sequence_count)) + + raise + finally: f.close() scope.clear() scope.send('display on') scope.check_last_command() - scope.close() elapsed = time.time() - time0 - print 'Completed %i events in %.3f seconds.' % (i+1, elapsed) - print 'Averaged %.5f seconds per acquisition.' % (elapsed/(i+1)) - print "Wrote to file '%s'." % filename + if i > 0: + print 'Completed %i events in %.3f seconds.' % (i, elapsed) + print 'Averaged %.5f seconds per acquisition.' % (elapsed/i) + print "Wrote to file '%s'." % filename if __name__ == '__main__': import optparse usage = "usage: %prog <filename/prefix> [-n] [-r]" parser = optparse.OptionParser(usage, version="%prog 0.1.0") - parser.add_option("-n", type="int", dest="events", + parser.add_option("-n", type="int", dest="nevents", help="number of events to store per run", default=1000) - parser.add_option("-r", type="int", dest="runs", + parser.add_option("-r", type="int", dest="nruns", help="number of runs", default=1) parser.add_option("--time", action="store_true", dest="time", help="append time string to filename", default=False) @@ -141,16 +161,19 @@ if __name__ == '__main__': if len(args) < 1: sys.exit(parser.format_help()) - if options.events < 1 or options.runs < 1: + if options.nevents < 1 or options.nruns < 1: sys.exit("Please specify a number >= 1 for number of events/runs") - if options.runs == 1 and not options.time: - fetch(args[0], options.events) + if options.nruns == 1 and not options.time: + try: + fetch(args[0], options.nevents) + except KeyboardInterrupt: + pass else: import time import string - for i in range(options.runs): + for i in range(options.nruns): timestr = string.replace(time.asctime(time.localtime()), ' ', '-') filename = args[0] + '_' + timestr + '.hdf5' print '-' * 65 @@ -158,6 +181,6 @@ if __name__ == '__main__': print '-' * 65 try: - fetch(filename, options.events) + fetch(filename, options.nevents) except KeyboardInterrupt: break @@ -20,7 +20,7 @@ import array import StringIO import struct import numpy as np -import scope +import sock # data types in lecroy binary blocks, where: # length -- byte length of type @@ -123,21 +123,12 @@ wavedesc_template = ( ('descriptor_name' , 0 , String), ('acq_vert_offset' , 340 , Float), ('wave_source' , 344 , Enum) ) -class LeCroyScope(scope.Scope): +class LeCroyScope(sock.Socket): """ A class for triggering and fetching waveforms from the oscilloscope. - - .. note:: - - You must call connect() on the object before fetching waveforms. """ - def __init__(self, host, port=1861, timeout=2.0): - super(LeCroyScope, self).__init__(host, port, timeout) - - def connect(self): - """Connect to the oscilloscope.""" - super(LeCroyScope, self).connect() - # initialize some parameters + def __init__(self, *args, **kwargs): + super(LeCroyScope, self).__init__(*args, **kwargs) self.send('comm_header short') self.check_last_command() self.send('comm_format DEF9,BYTE,BIN') @@ -160,7 +151,7 @@ class LeCroyScope(scope.Scope): if channel not in range(1, 5): raise Exception('channel must be in %s.' % str(range(1, 5))) - self.send('c%s:wf? all' % str(channel)) + self.send('c%s:wf? desc' % str(channel)) msg = self.recv() if not int(msg[1]) == channel: @@ -207,8 +198,8 @@ class LeCroyScope(scope.Scope): def getwaveform(self, channel, wavedesc): """ - Request, process, and return the x and y arrays for channel number - *channel* from the oscilloscope. + Request, process, and return the voltage array for channel number + `channel` from the oscilloscope as a numpy array. """ if channel not in range(1, 5): diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..99572cd --- /dev/null +++ b/setup.py @@ -0,0 +1 @@ +scope_ip = '128.91.41.179' @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +import struct +import socket +import os + +headerformat = '>BBBBL' + +errors = { 1 : 'unrecognized command/query header', + 2 : 'illegal header path', + 3 : 'illegal number', + 4 : 'illegal number suffix', + 5 : 'unrecognized keyword', + 6 : 'string error', + 7 : 'GET embedded in another message', + 10 : 'arbitrary data block expected', + 11 : 'non-digit character in byte count field of arbitrary data ' + 'block', + 12 : 'EOI detected during definite length data block transfer', + 13 : 'extra bytes detected during definite length data block ' + 'transfer' } + +class Socket(object): + def __init__(self, host, port=1861, timeout=5.0): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((host, port)) + self.sock.settimeout(timeout) + + def clear(self, timeout=0.5): + """ + Clear any bytes in the oscilloscope's output queue by receiving + packets until the connection blocks for more than `timeout` seconds. + """ + t = self.sock.gettimeout() + self.sock.settimeout(timeout) + try: + while True: + self.sock.recv(4096) + except socket.timeout: + pass + self.sock.settimeout(t) + + def send(self, msg): + """Format and send the string `msg`.""" + if not msg.endswith('\n'): + msg += '\n' + header = struct.pack(headerformat, 129, 1, 1, 0, len(msg)) + self.sock.sendall(header + msg) + + def check_last_command(self): + """ + Check that the last command sent was received okay; if not, raise + an exception with details about the error. + """ + self.send('cmr?') + err = int(self.recv().split(' ')[-1].rstrip('\n')) + + if err in errors: + self.sock.close() + raise Exception(errors[err]) + + def recv(self): + """Return a message from the scope.""" + + reply = '' + while True: + header = '' + + while len(header) < 8: + header += self.sock.recv(8 - len(header)) + + operation, headerver, seqnum, spare, totalbytes = \ + struct.unpack(headerformat, header) + + buffer = '' + + while len(buffer) < totalbytes: + buffer += self.sock.recv(totalbytes - len(buffer)) + + reply += buffer + + if operation % 2: + break + + return reply + + def __del__(self): + self.sock.close() + +if __name__ == '__main__': + import sys + import setup + + sock = Socket(setup.scope_ip) + sock.clear() + + for msg in sys.argv[1:]: + sock.send(msg) + + if '?' in msg: + print repr(sock.recv()) + + sock.check_last_command() @@ -31,7 +31,7 @@ mg = {} for dataset in [f[name] for name in f]: mg[dataset.name] = TMultiGraph() - samples = dataset.attrs['wave_array_count'] + samples = dataset.attrs['wave_array_count']//dataset.attrs['nom_subarray_count'] dx = dataset.attrs['horiz_interval'] dy = dataset.attrs['vertical_gain'] xoffset = dataset.attrs['horiz_offset'] |