Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OmniScan feature ideas - Changing to continuous counter outputs (allowing for M-Series) #509

Open
michaelb1886 opened this issue Apr 30, 2019 · 1 comment
Projects

Comments

@michaelb1886
Copy link
Contributor

michaelb1886 commented Apr 30, 2019

What is affected by this bug?

The reason X series NI cards with 4 counters are required is because the clock outputs for scanning and counting are finite in length - requiring two counters.

When does this occur?

For any NIDAQ counting or scanning operation.

Where on the platform does it happen?

How do we replicate the issue?

Expected behavior (i.e. solution)

Change the clock outputs to be continuous, and make the analog output, and counter input that depend on the clock timing have finite timing. This allows all M series cards to be used. I was going to rewrite the nidaq hardware file as such, but I soon learnt that the number of counters was too tied the confocal logic to make the effort worth the pay-off.

Other Comments

I understnad this is probably not a major priority, but if the the moniscan project is a major rethink of the confocal scanning behaviour then this would be a good time to implement what I think is an obviously good idea.

Here is an example from the Stuttgart code that uses 2 counters for all scanning/counting - it works well.

class CounterBoard:
    """nidaq Counter board.
    """

    _CountAverageLength = 10

    _MaxCounts = 1e7

    _DefaultCountLength = 1000

    _RWTimeout = 1.0

    def __init__(self, CounterIn, CounterOut, TickSource, SettlingTime=2e-3, CountTime=8e-3):
        self._CODevice = CounterOut
        self._CIDevice = CounterIn

        self._PulseTrain = self._CODevice+'InternalOutput' # counter bins are triggered by CTR1
        self._TickSource = TickSource #the signal: ticks coming from the APDs

        # nidaq Tasks
        self.COTask = ctypes.c_ulong()
        self.CITask = ctypes.c_ulong()
        CHK(  dll.DAQmxCreateTask('', ctypes.byref(self.COTask))  )
        CHK(  dll.DAQmxCreateTask('', ctypes.byref(self.CITask))  )

        f = 1. / ( CountTime + SettlingTime )
        DutyCycle = CountTime * f

        # ctr1 generates a continuous square wave with given duty cycle. This serves simultaneously
        # as sampling clock for AO (update DAC at falling edge), and as gate for counter (count between
        # rising and falling edge)
        CHK(  dll.DAQmxCreateCOPulseChanFreq( self.COTask,
                                              self._CODevice, '',
                                              DAQmx_Val_Hz, DAQmx_Val_Low, ctypes.c_double(0),
                                              ctypes.c_double(f),
                                              ctypes.c_double(DutyCycle) )  )

        # ctr0 is used to count photons. Used to count ticks in N+1 gates
        CHK(  dll.DAQmxCreateCIPulseWidthChan( self.CITask,
                                               self._CIDevice, '',
                                               ctypes.c_double(0),
                                               ctypes.c_double(self._MaxCounts*DutyCycle/f),
                                               DAQmx_Val_Ticks, DAQmx_Val_Rising, '')   )

        CHK(  dll.DAQmxSetCIPulseWidthTerm( self.CITask, self._CIDevice, self._PulseTrain )  )
        CHK(  dll.DAQmxSetCICtrTimebaseSrc( self.CITask, self._CIDevice, self._TickSource )  )

        self._SettlingTime = None
        self._CountTime = None
        self._DutyCycle = None
        self._f = None
        self._CountSamples = self._DefaultCountLength
        
        self.setTiming(SettlingTime, CountTime)

        self._CINread = ctypes.c_int32()

        self.setCountLength(self._DefaultCountLength)

    def setCountLength(self, N, BufferLength=None, SampleLength=None):
        """
        Set the number of counter samples / length of pulse train. If N is finite, a finite pulse train
        of length N is generated and N count samples are acquired. If N is infinity, an infinite pulse
        train is generated. BufferLength and SampleLength specify the length of the buffer and the length
        of a sample that is read in one read operation. In this case, always the most recent samples are read.
        """
        if N < numpy.inf:
            CHK(  dll.DAQmxCfgImplicitTiming( self.COTask, DAQmx_Val_ContSamps, ctypes.c_ulonglong(N))  )
            CHK(  dll.DAQmxCfgImplicitTiming( self.CITask, DAQmx_Val_FiniteSamps, ctypes.c_ulonglong(N))  )
            # read samples from beginning of acquisition, do not overwrite
            CHK( dll.DAQmxSetReadRelativeTo(self.CITask, DAQmx_Val_CurrReadPos) )
            CHK( dll.DAQmxSetReadOffset(self.CITask, 0) )
            CHK( dll.DAQmxSetReadOverWrite(self.CITask, DAQmx_Val_DoNotOverwriteUnreadSamps) )
            self._CountSamples = N
            self._TaskTimeout = 4 * N / self._f
        else:
            CHK(  dll.DAQmxCfgImplicitTiming( self.COTask, DAQmx_Val_ContSamps, ctypes.c_ulonglong(BufferLength))  )
            CHK(  dll.DAQmxCfgImplicitTiming( self.CITask, DAQmx_Val_ContSamps, ctypes.c_ulonglong(BufferLength))  )
            # read most recent samples, overwrite buffer
            CHK( dll.DAQmxSetReadRelativeTo(self.CITask, DAQmx_Val_MostRecentSamp) )
            CHK( dll.DAQmxSetReadOffset(self.CITask, -SampleLength) )
            CHK( dll.DAQmxSetReadOverWrite(self.CITask, DAQmx_Val_OverwriteUnreadSamps) )
            self._CountSamples = SampleLength
        self._CountLength = N
        self._CIData = numpy.empty((self._CountSamples,), dtype=numpy.uint32)

    def CountLength(self):
        return self._CountLength

    def setTiming(self, SettlingTime, CountTime):
        if SettlingTime != self._SettlingTime or CountTime != self._CountTime: 
            f = 1. / ( CountTime + SettlingTime )
            DutyCycle = CountTime * f
            CHK( dll.DAQmxSetCOPulseFreq( self.COTask, self._CODevice, ctypes.c_double(f)  )  )
            CHK( dll.DAQmxSetCOPulseDutyCyc( self.COTask, self._CODevice, ctypes.c_double(DutyCycle)  )   )
            self._SettlingTime = SettlingTime
            self._CountTime = CountTime
            self._f = f
            self._DutyCycle = DutyCycle
            if self._CountSamples is not None:
                self._TaskTimeout = 4 * self._CountSamples / self._f

    def getTiming(self):
        return self._SettlingTime, self._CountTime

    def StartCO(self):
        CHK( dll.DAQmxStartTask(self.COTask) )
    def StartCI(self):
        CHK( dll.DAQmxStartTask(self.CITask) )

    def StopCO(self):
        CHK( dll.DAQmxStopTask(self.COTask) )
    def StopCI(self):
        CHK( dll.DAQmxStopTask(self.CITask) )

    def ReadCI(self):
        CHK( dll.DAQmxReadCounterU32(self.CITask
                                     , ctypes.c_int32(self._CountSamples)
                                     , ctypes.c_double(self._RWTimeout)
                                     , self._CIData.ctypes.data_as(c_uint32_p)
                                     , ctypes.c_uint32(self._CountSamples)
                                     , ctypes.byref(self._CINread), None) )
        return self._CIData

    def WaitCI(self):
        CHK( dll.DAQmxWaitUntilTaskDone(self.CITask, ctypes.c_double(self._TaskTimeout))  )

    def startCounter(self, SettlingTime, CountTime):
        if self.CountLength() != numpy.inf:
            self.setCountLength(numpy.inf, max(1000, self._CountAverageLength), self._CountAverageLength)
        self.setTiming(SettlingTime, CountTime)
        self.StartCI()
        self.StartCO()
        time.sleep(self._CountSamples / self._f)

    def Count(self):
        """Return a single count."""
        return self.ReadCI().mean() * self._f / self._DutyCycle

    def stopCounter(self):
        self.StopCI()
        self.StopCO()

#    def __del__(self):
#        CHK( dll.DAQmxClearTask(self.CITask) )
#        CHK( dll.DAQmxClearTask(self.COTask) )


class MultiBoard( CounterBoard ):
    """nidaq Multifuntion board."""

    _DefaultAOLength = 1000

    def __init__(self, CounterIn, CounterOut, TickSource, AOChannels, v_range=(0.,10.)):
        CounterBoard.__init__(self, CounterIn, CounterOut, TickSource)
        self._AODevice = AOChannels
        self.AOTask = ctypes.c_ulong()
        CHK(  dll.DAQmxCreateTask('', ctypes.byref(self.AOTask))  )
        CHK(  dll.DAQmxCreateAOVoltageChan( self.AOTask,
                                            self._AODevice, '',
                                            ctypes.c_double(v_range[0]),
                                            ctypes.c_double(v_range[1]),
                                            DAQmx_Val_Volts,'')    )
        self._AONwritten = ctypes.c_int32()

        self.setAOLength(self._DefaultAOLength)

    def setAOLength(self, N):
        if N == 1:
            CHK( dll.DAQmxSetSampTimingType( self.AOTask, DAQmx_Val_OnDemand)  )
        else:
            CHK( dll.DAQmxSetSampTimingType( self.AOTask, DAQmx_Val_SampClk)  )
            if N < numpy.inf:
                CHK( dll.DAQmxCfgSampClkTiming( self.AOTask,
                                                self._PulseTrain,
                                                ctypes.c_double(self._f),
                                                DAQmx_Val_Falling, DAQmx_Val_FiniteSamps,
                                                ctypes.c_ulonglong(N)) )
        self._AOLength = N

    def AOLength(self):
        return self._AOLength

    def StartAO(self):
        CHK( dll.DAQmxStartTask(self.AOTask) )

    def StopAO(self):
        CHK( dll.DAQmxStopTask(self.AOTask) )

    def WriteAO(self, data, start=False):
        CHK( dll.DAQmxWriteAnalogF64( self.AOTask,
                                      ctypes.c_int32(self._AOLength),
                                      start,
                                      ctypes.c_double(self._RWTimeout),
                                      DAQmx_Val_GroupByChannel,
                                      data.ctypes.data_as(c_float64_p),
                                      ctypes.byref(self._AONwritten), None) )
        return self._AONwritten.value
    
class AOBoard():
    """nidaq Multifuntion board."""    
    
    def __init__(self, AOChannels):
        self._AODevice = AOChannels
        self.Task = ctypes.c_ulong()
        CHK(  dll.DAQmxCreateTask('', ctypes.byref(self.Task))  )
        CHK(  dll.DAQmxCreateAOVoltageChan( self.Task,
                                            self._AODevice, '',
                                            ctypes.c_double(0.),
                                            ctypes.c_double(10.),
                                            DAQmx_Val_Volts,'')    )
        CHK( dll.DAQmxSetSampTimingType( self.Task, DAQmx_Val_OnDemand)  )
        self._Nwritten = ctypes.c_int32()

    def Write(self, data):
        CHK( dll.DAQmxWriteAnalogF64(self.Task,
                                     ctypes.c_long(1),
                                     1,
                                     ctypes.c_double(1.0),
                                     DAQmx_Val_GroupByChannel,
                                     data.ctypes.data_as(c_float64_p),
                                     ctypes.byref(self._Nwritten),
                                     None) )

    def Start(self):
        CHK( dll.DAQmxStartTask(self.Task)  )

    def Wait(self, timeout):
        CHK( dll.DAQmxWaitUntilTaskDone(self.Task, ctypes.c_double(timeout)) )

    def Stop(self):
        CHK( dll.DAQmxStopTask(self.Task)  )

    def __del__(self):
        CHK( dll.DAQmxClearTask(self.Task)  )


class Scanner( MultiBoard ):

    def __init__(self, CounterIn, CounterOut, TickSource, AOChannels,
                 x_range, y_range, z_range, v_range=(0.,10.),
                 invert_x=False, invert_y=False, invert_z=False, swap_xy=False, TriggerChannels=None):
        MultiBoard.__init__(self, CounterIn=CounterIn,
                                  CounterOut=CounterOut,
                                  TickSource=TickSource,
                                  AOChannels=AOChannels,
                                  v_range=v_range)
        if TriggerChannels is not None:
            self._trigger_task = DOTask(TriggerChannels)
        self.xRange = x_range
        self.yRange = y_range
        self.zRange = z_range
        self.vRange = v_range
        self.x = 0.0
        self.y = 0.0
        self.z = 0.0
        self.invert_x = invert_x
        self.invert_y = invert_y
        self.invert_z = invert_z
        self.swap_xy = swap_xy

    def getXRange(self):
        return self.xRange

    def getYRange(self):
        return self.yRange

    def getZRange(self):
        return self.zRange

    def setx(self, x):
        """Move stage to x, y, z
        """
        if self.AOLength() != 1:
            self.setAOLength(1)
        self.WriteAO(self.PosToVolt((x, self.y, self.z)), start=True)
        self.x = x

    def sety(self, y):
        """Move stage to x, y, z
        """
        if self.AOLength() != 1:
            self.setAOLength(1)
        self.WriteAO(self.PosToVolt((self.x, y, self.z)), start=True)
        self.y = y

    def setz(self, z):
        """Move stage to x, y, z
        """
        if self.AOLength() != 1:
            self.setAOLength(1)
        self.WriteAO(self.PosToVolt((self.x, self.y, z)), start=True)
        self.z = z

    def scanLine(self, Line, SecondsPerPoint, return_speed=None):
        """Perform a line scan. If return_speed is not None, return to beginning of line
        with a speed 'return_speed' times faster than the speed currently set.
        """
        self.setTiming(SecondsPerPoint*0.1, SecondsPerPoint*0.9)

        N = Line.shape[1]

        if self.AOLength() != N: # set buffers of nidaq Tasks, data read buffer and timeout if needed
            self.setAOLength(N)

        if self.CountLength() != N+1:
            self.setCountLength(N+1)

        # send line start trigger
        if hasattr(self, '_trigger_task'):
            self._trigger_task.Write(numpy.array((1,0), dtype=numpy.uint8) )
            time.sleep(0.001)
            self._trigger_task.Write(numpy.array((0,0), dtype=numpy.uint8) )

        # acquire line
        self.WriteAO( self.PosToVolt(Line) )

        self.StartAO()
        self.StartCI()
        self.StartCO()

        self.WaitCI()

        # send line stop trigger
        if hasattr(self, '_trigger_task'):
            self._trigger_task.Write(numpy.array((0,1), dtype=numpy.uint8) )
            time.sleep(0.001)
            self._trigger_task.Write(numpy.array((0,0), dtype=numpy.uint8) )

        data = self.ReadCI()

        self.StopAO()
        self.StopCI()
        self.StopCO()

        if return_speed is not None:
            self.setTiming(SecondsPerPoint*0.5/return_speed, SecondsPerPoint*0.5/return_speed)
            self.WriteAO( self.PosToVolt(Line[:,::-1]) )
            self.StartAO()
            self.StartCI()
            self.StartCO()
            self.WaitCI()
            self.StopAO()
            self.StopCI()
            self.StopCO()
        self.setTiming(SecondsPerPoint*0.1, SecondsPerPoint*0.9)

        return data[1:] * self._f / self._DutyCycle

    def setPosition(self, x, y, z):
        """Move stage to x, y, z"""
        if self.AOLength() != 1:
            self.setAOLength(1)
        self.WriteAO(self.PosToVolt((x, y, z)), start=True)
        self.x, self.y, self.z = x, y, z

    def PosToVolt(self, r):
        x = self.xRange
        y = self.yRange
        z = self.zRange
        v = self.vRange
        v0 = v[0]
        dv = v[1]-v[0]
        if self.invert_x:
            vx = v0+(x[1]-r[0])/(x[1]-x[0])*dv            
        else:
            vx = v0+(r[0]-x[0])/(x[1]-x[0])*dv            
        if self.invert_y:
            vy = v0+(y[1]-r[1])/(y[1]-y[0])*dv            
        else:
            vy = v0+(r[1]-y[0])/(y[1]-y[0])*dv            
        if self.invert_z:
            vz = v0+(z[1]-r[2])/(z[1]-z[0])*dv            
        else:
            vz = v0+(r[2]-z[0])/(z[1]-z[0])*dv
        if self.swap_xy:
            vt = vx
            vx = vy
            vy = vt            
        return numpy.vstack( (vx,vy,vz) )
@Neverhorst
Copy link
Member

Thank you very much for your input, @michaelb1886.
The way scanning is currently realized using NI cards is indeed not the optimal solution. This has already been discussed and is on our agenda for omniscan. In principle it should not matter HOW exactly the hardware module is performing the scan as long as it complies with the interface to the controlling logic module. The problem is that the current scanner logic is explicitly handling hardware specific implementation details which is going against the qudi idea of abstracted hardware.

@LukePiewalker LukePiewalker added this to ToDo in Omniscan Apr 19, 2021
@LukePiewalker LukePiewalker moved this from ToDo to Done in Omniscan Mar 14, 2022
@LukePiewalker LukePiewalker moved this from Done to Old Core Things in Omniscan Mar 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Omniscan
Old Core Things
Development

No branches or pull requests

2 participants