qudi
qudi copied to clipboard
OmniScan feature ideas - Changing to continuous counter outputs (allowing for M-Series)
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) )
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.