Пример #1
0
    def __init__(self, prefix, scaler=None, nchan=8, clockrate=50.0):
        self._nchan = nchan
        self.scaler = None
        self.clockrate = clockrate  # clock rate in MHz
        self._mode = SCALER_MODE

        if scaler is not None:
            self.scaler = Scaler(scaler, nchan=nchan)

        self.mcas = []
        for i in range(nchan):
            self.mcas.append(MCA(prefix, mca=i + 1, nrois=2))

        Device.__init__(self,
                        prefix,
                        delim='',
                        attrs=self.attrs,
                        mutable=False)

        time.sleep(0.05)
        for pvname, pv in self._pvs.items():
            pv.get()

        self.ast_interp = asteval.Interpreter()
        self.read_scaler_config()
Пример #2
0
    def __init__(self, prefix, scaler=None, nchan=8, clockrate=50.0):
        if not prefix.endswith(':'):
            prefix = "%s:" % prefix
        self._nchan = nchan
        self.scaler = None
        self.clockrate = clockrate # clock rate in MHz
        self._mode = SCALER_MODE

        if scaler is not None:
            self.scaler = Scaler(scaler, nchan=nchan)

        self.mcas = []
        for i in range(nchan):
            self.mcas.append(MCA(prefix, mca=i+1, nrois=2))

        Device.__init__(self, prefix, delim='',
                        attrs=self.attrs, mutable=False)

        time.sleep(0.05)
        for pvname, pv in self._pvs.items():
            pv.get()
Пример #3
0
    def __init__(self, prefix, scaler=None, nchan=8, clockrate=50.0):
        self._nchan = nchan
        self.scaler = None
        self.clockrate = clockrate # clock rate in MHz
        self._mode = SCALER_MODE

        if scaler is not None:
            self.scaler = Scaler(scaler, nchan=nchan)

        self.mcas = []
        for i in range(nchan):
            self.mcas.append(MCA(prefix, mca=i+1, nrois=2))

        Device.__init__(self, prefix, delim='',
                        attrs=self.attrs, mutable=False)

        time.sleep(0.05)
        for pvname, pv in self._pvs.items():
            pv.get()

        self.ast_interp = asteval.Interpreter()
        self.read_scaler_config()
Пример #4
0
class Struck(Device):
    """
    Very simple implementation of Struck SIS MultiChannelScaler
    """
    attrs = (
        'ChannelAdvance',
        'Prescale',
        'EraseStart',
        'EraseAll',
        'StartAll',
        'StopAll',
        'PresetReal',
        'ElapsedReal',
        'Dwell',
        'Acquiring',
        'NuseAll',
        'MaxChannels',
        'CurrentChannel',
        'CountOnStart',  # InitialChannelAdvance',
        'SoftwareChannelAdvance',
        'Channel1Source',
        'ReadAll',
        'DoReadAll',
        'Model',
        'Firmware')

    _nonpvs = ('_prefix', '_pvs', '_delim', '_nchan', 'clockrate', 'scaler',
               'mcas', 'ast_interp')

    def __init__(self, prefix, scaler=None, nchan=8, clockrate=50.0):
        self._nchan = nchan
        self.scaler = None
        self.clockrate = clockrate  # clock rate in MHz
        self._mode = SCALER_MODE

        if scaler is not None:
            self.scaler = Scaler(scaler, nchan=nchan)

        self.mcas = []
        for i in range(nchan):
            self.mcas.append(MCA(prefix, mca=i + 1, nrois=2))

        Device.__init__(self,
                        prefix,
                        delim='',
                        attrs=self.attrs,
                        mutable=False)

        time.sleep(0.05)
        for pvname, pv in self._pvs.items():
            pv.get()

        self.ast_interp = asteval.Interpreter()
        self.read_scaler_config()

    def ExternalMode(self,
                     countonstart=None,
                     initialadvance=None,
                     realtime=0,
                     prescale=1,
                     trigger_width=None):
        """put Struck in External Mode, with the following options:
        option            meaning                   default value
        ----------------------------------------------------------
        countonstart    set Count on Start             None
        initialadvance  set Initial Channel Advance    None
        reatime         set Preset Real Time           0
        prescale        set Prescale value             1
        trigger_width   set trigger width in sec       None

        here, `None` means "do not change from current value"
        """
        out = self.put('ChannelAdvance', 1)  # external
        if self.scaler is not None:
            self.scaler.put('CONT', 0, wait=True)
        if realtime is not None:
            self.put('PresetReal', realtime)
        if prescale is not None:
            self.put('Prescale', prescale)
        if countonstart is not None:
            self.put('CountOnStart', countonstart)
        if initialadvance is not None:
            self.put('InitialChannelAdvancel', initialadvance)
        if trigger_width is not None:
            self.put('LNEOutputWidth', trigger_width)
        time.sleep(0.002)
        return out

    def InternalMode(self, prescale=None):
        "put Struck in Internal Mode"
        out = self.put('ChannelAdvance', 0)  # internal
        if self.scaler is not None:
            self.scaler.put('CONT', 0, wait=True)
        if prescale is not None:
            self.put('Prescale', prescale)
        time.sleep(0.002)
        return out

    def set_dwelltime(self, val):
        "Set Dwell Time"
        # print("Struck DwellTime ", self._pvs['Dwell'], val)
        if isinstance(val, (list, tuple, numpy.ndarray)):
            val = val[0]

        if val is not None:
            self.put('Dwell', val)

    def ContinuousMode(self, dwelltime=None, numframes=None):
        """set to continuous mode: use for live reading

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [None]
        numframes (None or int):   number of frames to collect [None]

    Notes:
        1. This sets AquireMode to Continuous.  If dwelltime or numframes
           is not None, they will be set
        """
        self.InternalMode()
        if numframes is not None:
            self.put('NuseAll', numframes, wait=True)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        if self.scaler is not None:
            self.scaler.put('CONT', 1, wait=True)
        self._mode = SCALER_MODE

    def ScalerMode(self, dwelltime=1.0, numframes=1):
        """ set to scaler mode: ready for step scanning

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [1.0]
        numframes (None or int):   number of frames to collect [1]

    Notes:
        1. numframes should be 1, unless you know what you're doing.
        """
        if numframes is not None:
            self.put('NuseAll', numframes, wait=True)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        if self.scaler is not None:
            self.scaler.put('CONT', 0, wait=True)
        self._mode = SCALER_MODE

    def NDArrayMode(self,
                    dwelltime=None,
                    numframes=None,
                    countonstart=True,
                    trigger_width=None):
        """ set to array mode: ready for slew scanning

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [0.25]
        numframes (None or int):   number of frames to collect [8192]
        countonstart (None or bool):  whether to count on start [True]
        trigger_width (None or float):   output trigger width (in seconds)
             for optional SIS 3820 [None]

    Notes:
        1. this arms SIS to be ready for slew scanning.
        2. setting dwelltime or numframes to None is discouraged,
           as it can lead to inconsistent data arrays.

        """
        # print("Struck ArrayMode ", dwelltime, numframes)
        if numframes is not None:
            self.put('NuseAll', numframes)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        self._mode = NDARRAY_MODE

        time.sleep(0.01)
        self.ExternalMode(trigger_width=trigger_width,
                          countonstart=countonstart)

    def ROIMode(self,
                dwelltime=None,
                numframes=None,
                countonstart=True,
                trigger_width=None):
        """set to ROI mode: ready for slew scanning"""
        self.NDArrayMode(dwelltime=dwelltime,
                         numframes=numframes,
                         countonstart=countonstart,
                         trigger_width=trigger_width)

    def start(self, wait=False):
        "Start Struck"
        if self.scaler is not None:
            self.scaler.put('CONT', 0, wait=True)
        return self.put('EraseStart', 1, wait=wait)

    def stop(self):
        "Stop Struck Collection"
        if self.get('Acquiring'):
            self.put('StopAll', 1)
        return

    def erase(self):
        "Start Struck"
        return self.put('EraseAll', 1)

    def mcaNread(self, nmcas=1):
        "Read a Struck MCA"
        return self.get('mca%i.NORD' % nmcas)

    def readmca(self, nmca=1, count=None):
        "Read a Struck MCA"
        return self.get('mca%i' % nmca, count=count)

    def read_all_mcas(self):
        return [self.readmca(nmca=i + 1) for i in range(self._nchan)]

    def read_scaler_config(self):
        """read names and calcs for scaler channels"""
        if self.scaler is None:
            return []
        conf = []
        for n in range(1, self._nchan + 1):
            name = self.scaler.get('NM%i' % n).strip()
            if len(name) > 0:
                name = name.strip().replace(' ', '_')
                calc = self.scaler.get('expr%i' % n)
                conf.append((n, name, calc))
        return conf

    def save_arraydata(self, filename='sis.dat', npts=None, **kws):
        "save MCA spectra to ASCII file"
        t0 = time.time()
        # print("SIS Save Array Data ", filename, os.getcwd())
        rdata, sdata, names, calcs, fmts = [], [], [], [], []
        headers = []
        if npts is None:
            npts = self.NuseAll
        npts_req = npts
        avars = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H')
        adat = {}
        for name in avars:
            self.ast_interp.symtable[name] = adat[name] = numpy.zeros(npts)
        scaler_config = self.read_scaler_config()

        # read MCAs until all data have a consistent length (up to ~2 seconds)
        t0 = time.time()
        time.sleep(0.025)
        for _i in range(10):
            npts_chan = []
            for nchan, name, calc in scaler_config:
                dat = self.readmca(nmca=nchan)
                if (dat is None or not isinstance(dat, numpy.ndarray)):
                    dat = []
                npts_chan.append(len(dat))
            if npts_req is None:
                npts_req = npts_chan[0]
            if (npts_chan[0] == npts_req) and (max(npts_chan)
                                               == min(npts_chan)):
                break
            time.sleep(0.025 * (_i + 1))

        if max(npts_chan) != min(npts_chan):
            print(" Struck warning, inconsistent number of points!")
            print(" -- ", npts_chan)

        # make sure all data is the same length for calcs
        npts = min(npts, min(npts_chan))
        #print(" Struck save_array: read %i in %.3f sec" % (npts, time.time()-t0))

        # final read
        icol = 0
        hformat = "# Column.%i: %16s | %s"
        for nchan, name, calc in scaler_config:
            icol += 1
            dat = self.readmca(nmca=nchan)
            varname = avars[nchan - 1]
            adat[varname] = dat
            label = "%s | %s" % ("%smca%i" % (self._prefix, nchan), varname)
            if icol == 1 or len(calc) > 1:
                if icol == 1:
                    calc = 'A / 50.0'
                label = "calculated | %s" % calc
                rdata.append(("%s_raw" % name, nchan, varname, dat))

            headers.append(hformat % (icol, name, label))
            names.append(name)
            calcs.append(calc)
            fmt = ' {:14f} '
            if icol == 1:
                fmt = ' {:14.2f} '
            fmts.append(fmt)

        for key, val in adat.items():
            try:
                self.ast_interp.symtable[key] = val[:npts]
            except TypeError:
                self.ast_interp.symtable[key] = val

        for calc in calcs:
            result = self.ast_interp.eval(calc)
            if result is None:
                result = numpy.zeros(1)
            sdata.append(result)

        for name, nchan, varname, rdat in rdata:
            icol += 1
            label = "%s | %s" % ("%smca%i" % (self._prefix, nchan), varname)
            headers.append(hformat % (icol, name, label))
            names.append(name)
            sdata.append(rdat)
            fmts.append(' {:10.0f} ')

        try:
            sdata = numpy.array([s[:npts] for s in sdata]).transpose()
            npts, nmcas = sdata.shape
        except:
            return (0, 0)

        buff = [
            '# Struck MCA data: %s' % self._prefix,
            '# Nchannels, Nmcas = %i, %i' % (npts, nmcas),
            '# Time in microseconds'
        ]

        buff.extend(headers)
        buff.append("#%s" % ("-" * 60))
        buff.append("# %s" % ' | '.join(names))

        fmt = ''.join(fmts)
        for i in range(npts):
            buff.append(fmt.format(*sdata[i]))
        buff.append('')
        fout = open(filename, 'w')
        fout.write("\n".join(buff))
        fout.close()
        # print("SIS saved in %.3f seconds" % (time.time()-t0))
        return (nmcas, npts)
Пример #5
0
class Struck(Device):
    """
    Very simple implementation of Struck SIS MultiChannelScaler
    """
    attrs = ('ChannelAdvance', 'Prescale', 'EraseStart',
             'EraseAll', 'StartAll', 'StopAll',
             'PresetReal', 'ElapsedReal',
             'Dwell', 'Acquiring', 'NuseAll', 'MaxChannels',
             'CurrentChannel', 'CountOnStart',   # InitialChannelAdvance',
             'SoftwareChannelAdvance', 'Channel1Source',
             'ReadAll', 'DoReadAll', 'Model', 'Firmware')

    _nonpvs = ('_prefix', '_pvs', '_delim', '_nchan',
               'clockrate', 'scaler', 'mcas')

    def __init__(self, prefix, scaler=None, nchan=8, clockrate=50.0):
        if not prefix.endswith(':'):
            prefix = "%s:" % prefix
        self._nchan = nchan
        self.scaler = None
        self.clockrate = clockrate # clock rate in MHz
        self._mode = SCALER_MODE

        if scaler is not None:
            self.scaler = Scaler(scaler, nchan=nchan)

        self.mcas = []
        for i in range(nchan):
            self.mcas.append(MCA(prefix, mca=i+1, nrois=2))

        Device.__init__(self, prefix, delim='',
                        attrs=self.attrs, mutable=False)

        time.sleep(0.05)
        for pvname, pv in self._pvs.items():
            pv.get()

    def ExternalMode(self, countonstart=0, initialadvance=None,
                     realtime=0, prescale=1, trigger_width=None):
        """put Struck in External Mode, with the following options:
        option            meaning                   default value
        ----------------------------------------------------------
        countonstart    set Count on Start             0
        initialadvance  set Initial Channel Advance    None
        reatime         set Preset Real Time           0
        prescale        set Prescale value             1
        trigger_width   set trigger width in sec       None
        """
        out = self.put('ChannelAdvance', 1)  # external
        if self.scaler is not None:
            self.scaler.OneShotMode()
        if realtime is not None:
            self.put('PresetReal', realtime)
        if prescale is not None:
            self.put('Prescale', prescale)
        if countonstart is not None:
            self.put('CountOnStart', countonstart)
        if initialadvance is not None:
            self.put('InitialChannelAdvancel', initialadvance)
        if trigger_width is not None:
            self.put('LNEOutputWidth', trigger_width)

        return out

    def InternalMode(self, prescale=None):
        "put Struck in Internal Mode"
        out = self.put('ChannelAdvance', 0)  # internal
        if self.scaler is not None:
            self.scaler.OneShotMode()
        if prescale is not None:
            self.put('Prescale', prescale)
        return out

    def set_dwelltime(self, val):
        "Set Dwell Time"
        # print("Struck DwellTime ", self._pvs['Dwell'], val)
        if val is not None:
            self.put('Dwell', val)

    def ContinuousMode(self, dwelltime=None, numframes=None):
        """set to continuous mode: use for live reading

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [None]
        numframes (None or int):   number of frames to collect [None]

    Notes:
        1. This sets AquireMode to Continuous.  If dwelltime or numframes
           is not None, they will be set
        """
        self.InternalMode()
        if numframes is not None:
            self.put('NuseAll', numframes)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        if self.scaler is not None:
            self.scaler.AutoCountMode()
        self._mode = SCALER_MODE

    def ScalerMode(self, dwelltime=1.0, numframes=1):
        """ set to scaler mode: ready for step scanning

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [1.0]
        numframes (None or int):   number of frames to collect [1]

    Notes:
        1. numframes should be 1, unless you know what you're doing.
        """
        if numframes is not None:
            self.put('NuseAll', numframes)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        if self.scaler is not None:
            self.scaler.OneShotMode()
        self._mode = SCALER_MODE

    def NDArrayMode(self, dwelltime=None, numframes=None, trigger_width=None):
        """ set to array mode: ready for slew scanning

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [0.25]
        numframes (None int):   number of frames to collect [8192]
        trigger_width (None or float):   output trigger width (in seconds)
             for optional SIS 3820 [None]

    Notes:
        1. this arms SIS to be ready for slew scanning.
        2. setting dwelltime or numframes to None is discouraged,
           as it can lead to inconsistent data arrays.

        """
        # print("SIS NDArrayMode ", dwelltime, numframes)
        if numframes is not None:
            self.put('NuseAll', numframes)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        self._mode = NDARRAY_MODE

        time.sleep(0.05)
        self.ExternalMode(trigger_width=trigger_width, countonstart=False)

    def ROIMode(self, dwelltime=None, numframes=None, trigger_width=None):
        """set to ROI mode: ready for slew scanning"""
        self.NDArrayMode(dwelltime=dwelltime, numframes=numframes, trigger_width=trigger_width)
        self.put('CountOnStart', 1)

    def start(self, wait=False):
        "Start Struck"
        if self.scaler is not None:
            self.scaler.OneShotMode()
        return self.put('EraseStart', 1, wait=wait)

    def stop(self):
        "Stop Struck Collection"
        if self.get('Acquiring'):
            self.put('StopAll', 1)
        return

    def erase(self):
        "Start Struck"
        return self.put('EraseAll', 1)

    def mcaNread(self, nmcas=1):
        "Read a Struck MCA"
        return self.get('mca%i.NORD' % nmcas)

    def readmca(self, nmcas=1, count=None):
        "Read a Struck MCA"
        return self.get('mca%i' % nmcas, count=count)

    def read_all_mcas(self):
        return [self.readmca(nmcas=i+1) for i in range(self._nchan)]

    def save_arraydata(self, filename='Struck.dat', ignore_prefix=None, npts=None):
        "save MCA spectra to ASCII file"
        # print("SIS Save Array Data")
        sdata, names, addrs = [], [], []
        if npts is None:
            npts = self.MaxChannels
        time.sleep(0.025)
        nmca_chans = []
        for nchan in range(self._nchan):
            nmcas = nchan + 1
            _name = 'MCA%i' % nmcas
            _addr = '%s.mca%i' % (self._prefix, nmcas)
            time.sleep(0.002)
            if self.scaler is not None:
                scaler_name = self.scaler.get('NM%i' % nmcas)
                if scaler_name is not None:
                    _name = scaler_name.replace(' ', '_')
                    _addr = self.scaler._prefix + 'S%i' % nmcas
            if len(_name) < 1:
                continue
            read_ok = False
            ntries = 0
            while not read_ok and ntries < 10:
                ntries += 1
                mcadat = self.readmca(nmcas=nmcas)
                try:
                    nmca = len(mcadat)
                except:
                    nmca = 0
                if nmca >  npts-1:
                    read_ok = True
                else:
                    time.sleep(0.1)
                    continue
            # print("SIS Read Channel:  ", nchan, _name, nmca, ntries)
            if nmca > 2:
                names.append(_name)
                addrs.append(_addr)
                sdata.append(mcadat)
            nmca_chans.append(nmca)

        npts = min(npts, min(nmca_chans))
        # print("SIS Read Channels, npts = ", npts, nmca_chans)

        sdata = numpy.array([s[:npts] for s in sdata]).transpose()
        sdata[:, 0] = sdata[:, 0]/self.clockrate

        nelem, nmcas = sdata.shape
        npts = min(nelem, npts)

        addrs = ' | '.join(addrs)
        names = ' | '.join(names)
        formt = '%9i ' * nmcas + '\n'

        fout = open(filename, 'w')
        fout.write(HEADER % (self._prefix, npts, nmcas, addrs, names))
        for i in range(npts):
            fout.write(formt % tuple(sdata[i]))
        fout.close()
        return (nmcas, npts)
Пример #6
0
class Struck(Device):
    """
    Very simple implementation of Struck SIS MultiChannelScaler
    """
    attrs = ('ChannelAdvance', 'Prescale', 'EraseStart',
             'EraseAll', 'StartAll', 'StopAll',
             'PresetReal', 'ElapsedReal',
             'Dwell', 'Acquiring', 'NuseAll', 'MaxChannels',
             'CurrentChannel', 'CountOnStart',   # InitialChannelAdvance',
             'SoftwareChannelAdvance', 'Channel1Source',
             'ReadAll', 'DoReadAll', 'Model', 'Firmware')

    _nonpvs = ('_prefix', '_pvs', '_delim', '_nchan',
               'clockrate', 'scaler', 'mcas', 'ast_interp')

    def __init__(self, prefix, scaler=None, nchan=8, clockrate=50.0):
        self._nchan = nchan
        self.scaler = None
        self.clockrate = clockrate # clock rate in MHz
        self._mode = SCALER_MODE

        if scaler is not None:
            self.scaler = Scaler(scaler, nchan=nchan)

        self.mcas = []
        for i in range(nchan):
            self.mcas.append(MCA(prefix, mca=i+1, nrois=2))

        Device.__init__(self, prefix, delim='',
                        attrs=self.attrs, mutable=False)

        time.sleep(0.05)
        for pvname, pv in self._pvs.items():
            pv.get()

        self.ast_interp = asteval.Interpreter()
        self.read_scaler_config()

    def ExternalMode(self, countonstart=None, initialadvance=None,
                     realtime=0, prescale=1, trigger_width=None):
        """put Struck in External Mode, with the following options:
        option            meaning                   default value
        ----------------------------------------------------------
        countonstart    set Count on Start             None
        initialadvance  set Initial Channel Advance    None
        reatime         set Preset Real Time           0
        prescale        set Prescale value             1
        trigger_width   set trigger width in sec       None

        here, `None` means "do not change from current value"
        """
        out = self.put('ChannelAdvance', 1)  # external
        if self.scaler is not None:
            self.scaler.put('CONT', 0, wait=True)
        if realtime is not None:
            self.put('PresetReal', realtime)
        if prescale is not None:
            self.put('Prescale', prescale)
        if countonstart is not None:
            self.put('CountOnStart', countonstart)
        if initialadvance is not None:
            self.put('InitialChannelAdvancel', initialadvance)
        if trigger_width is not None:
            self.put('LNEOutputWidth', trigger_width)
        time.sleep(0.002)
        return out

    def InternalMode(self, prescale=None):
        "put Struck in Internal Mode"
        out = self.put('ChannelAdvance', 0)  # internal
        if self.scaler is not None:
            self.scaler.put('CONT', 0, wait=True)
        if prescale is not None:
            self.put('Prescale', prescale)
        time.sleep(0.002)
        return out

    def set_dwelltime(self, val):
        "Set Dwell Time"
        # print("Struck DwellTime ", self._pvs['Dwell'], val)
        if val is not None:
            self.put('Dwell', val)

    def ContinuousMode(self, dwelltime=None, numframes=None):
        """set to continuous mode: use for live reading

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [None]
        numframes (None or int):   number of frames to collect [None]

    Notes:
        1. This sets AquireMode to Continuous.  If dwelltime or numframes
           is not None, they will be set
        """
        self.InternalMode()
        if numframes is not None:
            self.put('NuseAll', numframes, wait=True)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        if self.scaler is not None:
            self.scaler.put('CONT', 1, wait=True)
        self._mode = SCALER_MODE

    def ScalerMode(self, dwelltime=1.0, numframes=1):
        """ set to scaler mode: ready for step scanning

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [1.0]
        numframes (None or int):   number of frames to collect [1]

    Notes:
        1. numframes should be 1, unless you know what you're doing.
        """
        if numframes is not None:
            self.put('NuseAll', numframes, wait=True)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        if self.scaler is not None:
            self.scaler.put('CONT', 0, wait=True)
        self._mode = SCALER_MODE

    def NDArrayMode(self, dwelltime=None, numframes=None, countonstart=True,
                    trigger_width=None):
        """ set to array mode: ready for slew scanning

    Arguments:
        dwelltime (None or float): dwelltime per frame in seconds [0.25]
        numframes (None or int):   number of frames to collect [8192]
        countonstart (None or bool):  whether to count on start [True]
        trigger_width (None or float):   output trigger width (in seconds)
             for optional SIS 3820 [None]

    Notes:
        1. this arms SIS to be ready for slew scanning.
        2. setting dwelltime or numframes to None is discouraged,
           as it can lead to inconsistent data arrays.

        """
        # print("Struck ArrayMode ", dwelltime, numframes)
        if numframes is not None:
            self.put('NuseAll', numframes)
        if dwelltime is not None:
            self.set_dwelltime(dwelltime)
        self._mode = NDARRAY_MODE

        time.sleep(0.01)
        self.ExternalMode(trigger_width=trigger_width, countonstart=countonstart)

    def ROIMode(self, dwelltime=None, numframes=None, countonstart=True,
                trigger_width=None):
        """set to ROI mode: ready for slew scanning"""
        self.NDArrayMode(dwelltime=dwelltime, numframes=numframes,
                         countonstart=countonstart, trigger_width=trigger_width)


    def start(self, wait=False):
        "Start Struck"
        if self.scaler is not None:
            self.scaler.put('CONT', 0, wait=True)
        return self.put('EraseStart', 1, wait=wait)

    def stop(self):
        "Stop Struck Collection"
        if self.get('Acquiring'):
            self.put('StopAll', 1)
        return

    def erase(self):
        "Start Struck"
        return self.put('EraseAll', 1)

    def mcaNread(self, nmcas=1):
        "Read a Struck MCA"
        return self.get('mca%i.NORD' % nmcas)

    def readmca(self, nmca=1, count=None):
        "Read a Struck MCA"
        return self.get('mca%i' % nmca, count=count)

    def read_all_mcas(self):
        return [self.readmca(nmca=i+1) for i in range(self._nchan)]

    def read_scaler_config(self):
        """read names and calcs for scaler channels"""
        if self.scaler is None:
            return []
        conf = []
        for n in range(1, self._nchan+1):
            name = self.scaler.get('NM%i' % n).strip()
            if len(name) > 0:
                name = name.strip().replace(' ', '_')
                calc = self.scaler.get('expr%i' % n)
                conf.append((n, name, calc))
        return conf

    def save_arraydata(self, filename='sis.dat', npts=None, **kws):
        "save MCA spectra to ASCII file"
        t0 = time.time()
        # print("SIS Save Array Data ", filename, os.getcwd())
        rdata, sdata, names, calcs, fmts = [], [], [], [], []
        headers = []
        if npts is None:
            npts = self.NuseAll
        npts_req = npts
        avars = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H')
        adat = {}
        for name in avars:
            self.ast_interp.symtable[name] = adat[name] = numpy.zeros(npts)
        scaler_config = self.read_scaler_config()

        # read MCAs until all data have a consistent length (up to ~2 seconds)
        t0 = time.time()
        time.sleep(0.025)
        for _i in range(10):
            npts_chan = []
            for nchan, name, calc in scaler_config:
                dat = self.readmca(nmca=nchan)
                if (dat is None or not isinstance(dat, numpy.ndarray)):
                    dat = []
                npts_chan.append(len(dat))
            if npts_req is None:
                npts_req = npts_chan[0]
            if (npts_chan[0] == npts_req) and (max(npts_chan) == min(npts_chan)):
                break
            time.sleep(0.025*(_i+1))

        if max(npts_chan) != min(npts_chan):
            print(" Struck warning, inconsistent number of points!")
            print(" -- ", npts_chan)

        # make sure all data is the same length for calcs
        npts = min(npts, min(npts_chan))
        #print(" Struck save_array: read %i in %.3f sec" % (npts, time.time()-t0))

        # final read
        icol = 0
        hformat = "# Column.%i: %16s | %s"
        for nchan, name, calc in scaler_config:
            icol += 1
            dat = self.readmca(nmca=nchan)
            varname = avars[nchan-1]
            adat[varname] = dat
            label = "%s | %s" % ("%smca%i" % (self._prefix, nchan), varname)
            if icol == 1 or len(calc) > 1:
                if icol == 1:
                    calc = 'A / 50.0'
                label = "calculated | %s" % calc
                rdata.append(("%s_raw" % name, nchan, varname, dat))

            headers.append(hformat % (icol, name, label))
            names.append(name)
            calcs.append(calc)
            fmt = ' {:14f} '
            if icol == 1:
                fmt = ' {:14.2f} '
            fmts.append(fmt)

        for key, val in adat.items():
            try:
                self.ast_interp.symtable[key] = val[:npts]
            except TypeError:
                self.ast_interp.symtable[key] = val

        for calc in calcs:
            result = self.ast_interp.eval(calc)
            if result is None:
                result = numpy.zeros(1)
            sdata.append(result)

        for name, nchan, varname, rdat in rdata:
            icol += 1
            label = "%s | %s" % ("%smca%i" % (self._prefix, nchan), varname)
            headers.append(hformat % (icol, name, label))
            names.append(name)
            sdata.append(rdat)
            fmts.append(' {:10.0f} ')

        try:
            sdata = numpy.array([s[:npts] for s in sdata]).transpose()
            npts, nmcas = sdata.shape
        except:
            return (0, 0)

        buff = ['# Struck MCA data: %s' % self._prefix,
                '# Nchannels, Nmcas = %i, %i' % (npts, nmcas),
                '# Time in microseconds']

        buff.extend(headers)
        buff.append("#%s" % ("-"*60))
        buff.append("# %s" % ' | '.join(names))

        fmt  = ''.join(fmts)
        for i in range(npts):
            buff.append(fmt.format(*sdata[i]))
        buff.append('')
        fout = open(filename, 'w')
        fout.write("\n".join(buff))
        fout.close()
        # print("SIS saved in %.3f seconds" % (time.time()-t0))
        return (nmcas, npts)