Adjusted to new pycomedi setup.
[pypiezo.git] / z_piezo.py
1 """The z_piezo module controls the one-dimensional displacement of a
2 piezoelectric actuator, while allowing simultaneous data aquisition on
3 two channels.  The module is called z_piezo, because for force spectroscopy,
4 the axis that needs simultaneous aquisition is the Z axis, although this
5 module could be used to provide control in any "fast" axis.
6
7 There are a few globals controlling the behavior of the entire module.
8
9   USE_ABCD_DEFLECTION (default False)
10     selects between using a preprocessed vertical deflection signal
11     and using the 'raw' 4 photodiode output segments.
12   TEXT_VERBOSE  (default False)
13     print text messages to stderr displaying actions taken
14   PYLAB_INTERACTIVE_VERBOSE (default False)
15     display pylab graphs of any ramps taken
16   BASE_FIG_NUM (default 50)
17     the figure number for z_piezo pylab figures will be this number + {0,1,..}
18   LOG_RAMPS (default False)
19     save the input and output of any ramps to the LOG_DIR directory
20   LOG_DIR = '/home/wking/rsrch/data/z_piezo'
21     where the ramp logs are saved to
22   DEFAULT_ZP_CHAN (default 0)
23     output channel controlling the z piezo
24   DEFAULT_ZP_MON_CHAN (default 1)
25     input channel monitoring the z piezo signal
26   DEFAULT_DEF_CHAN (default 0, or in ABCD mode (2,3,4,5))
27     input channel(s) monitoring deflection (or some other feedback signal)
28 """
29
30 USE_ABCD_DEFLECTION = False
31 TEXT_VERBOSE = False
32 PYLAB_INTERACTIVE_VERBOSE = False
33 BASE_FIG_NUM = 50
34 LOG_RAMPS = False
35 LOG_DIR = '/home/wking/rsrch/data/z_piezo'
36
37 # Hackish defaults for the calibcant.config module
38 DEFAULT_GAIN = 20 # Vpiezo / Voutput
39 DEFAULT_SENSITIVITY = 7.41 # nm_surface/Volt_piezo
40 DEFAULT_ZERO_PHOTODIODE_BITS = 2**15
41 DEFAULT_VPHOTO_IN_2_VOLTS = lambda vbits : (vbits-DEFAULT_ZERO_PHOTODIODE_BITS)/3276.8
42 DEFAULT_VZP_OUT_2_VOLTS   = lambda vbits : (vbits-DEFAULT_ZERO_PHOTODIODE_BITS)/3276.8
43
44 DEFAULT_ZP_CHAN = 0
45 DEFAULT_ZP_MON_CHAN = 1
46 DEFAULT_DEF_CHAN = 0
47 if USE_ABCD_DEFLECTION :
48     DEFAULT_DEF_CHAN = (2,3,4,5)
49     # The 4 photodiode segments are
50     #  A B
51     #  C D
52     # looking in along the laser
53
54 class NoComedi (ImportError):
55     "Missing comedi.  Don't initialize a z_piezo instance."
56     pass
57
58 try:
59     import pycomedi.single_aio as single_aio
60     import pycomedi.simult_aio as simult_aio
61     HAS_COMEDI = True
62 except ImportError, e:
63     HAS_COMEDI = False
64
65 from numpy import array, arange, ones, zeros, linspace, uint16, float, sin, pi
66 from scipy.stats import linregress
67 import data_logger
68
69 if PYLAB_INTERACTIVE_VERBOSE :
70     from pylab import figure, plot, title, legend, hold, subplot
71     import time # for timestamping lines on plots
72
73 class error (Exception) :
74     pass
75 class outOfRange (error) :
76     pass
77
78 # frontend:
79
80 class z_piezo :
81     def __init__(self, zp_chan=DEFAULT_ZP_CHAN,
82                  zp_mon_chan=DEFAULT_ZP_MON_CHAN,
83                  def_chan=DEFAULT_DEF_CHAN) :
84         if HAS_COMEDI is not True:
85             raise NoComedi
86         self.sensitivity = DEFAULT_SENSITIVITY
87         self.gain = DEFAULT_GAIN
88         self.nm2Vout = self.sensitivity * self.gain # nm_surface / V_output
89         self.chan_info = _chan_info(zp_chan, zp_mon_chan, def_chan)
90
91         self.curIn = array([0]*self.chan_info.numIn, dtype=uint16)
92         self.curOut = array([0]*self.chan_info.numOut, dtype=uint16)
93         self._jump = _z_piezo_jump(self.chan_info)
94         self._ramp = _z_piezo_ramp(self.chan_info)
95         self.zpMax = int(self.pos_nm2out(1000)) # limits at 1 micron
96         self.zpMin = int(self.pos_nm2out(-1000)) # and -1 micron
97         # define default value changed callbacks
98         self.setExternalZPiezo = None
99         self.setExternalZPiezoMon = None
100         self.setExternalDeflection = None
101     def __del__(self) :
102         pass
103     def curPos(self) :
104         return int(self.curOut[self.chan_info.zp_ind]) # cast as int so arithmetic is signed
105     def curPosMon(self) :
106         return int(self.curIn[self.chan_info.zp_mon_ind])
107     def curDef(self) :
108         if USE_ABCD_DEFLECTION :
109             A = int(self.curIn[self.chan_info.def_ind[0]])
110             B = int(self.curIn[self.chan_info.def_ind[1]])
111             C = int(self.curIn[self.chan_info.def_ind[2]])
112             D = int(self.curIn[self.chan_info.def_ind[3]])
113             df = float((A+B)-(C+D))/(A+B+C+D)
114             dfout = int(df * 2**15) + 2**15
115             if TEXT_VERBOSE :
116                 print "Current deflection %d (%d, %d, %d, %d)" \
117                     % (dfout, A, B, C, D)
118             return dfout
119         else : # reading the deflection voltage directly
120             return int(self.curIn[self.chan_info.def_ind])
121     def pos_out2V(self, out) :
122         return self._jump.out_to_phys(out)
123     def pos_V2out(self, V) :
124         # Vout is Volts output by the DAQ card
125         # and Vpiezo is Volts applied across the piezo
126         return self._jump.phys_to_out(V)
127     def pos_nm2out(self, nm) :
128         # Vout is Volts output by the DAQ card
129         # and Vpiezo is Volts applied across the piezo
130         return self.pos_V2out(nm / self.nm2Vout)
131     def pos_out2nm(self, out) :
132         return self.pos_out2V(out) * self.nm2Vout
133     def posMon_nm2out(self, nm) :
134         return self._jump.phys_to_out(nm / self.nm2Vout)
135     def posMon_out2nm(self, out) :
136         return self._jump.out_to_phys(out) * self.nm2Vout
137     def def_in2V(self, input) :
138         return self._jump.out_to_phys(input)
139     def def_V2in(self, physical) :
140         print physical, type(physical) # temporary debugging printout
141         return self._jump.phys_to_out(physical)
142     def curVals(self) :
143         return {'Z piezo output': self.curPos(),
144                 'Deflection input':self.curDef(),
145                 'Z piezo input':self.curPosMon()}
146     def pCurVals(self, curVals=None) :
147         if curVals == None :
148             curVals = self.curVals()
149         print "Z piezo output  : %6d" % curVals["Z piezo output"]
150         print "Z piezo input   : %6d" % curVals["Z piezo input"]
151         print "Deflection input: %6d" % curVals["Deflection input"]
152     def updateInputs(self) :
153         self.curIn = self._jump.updateInputs()
154         if self.setExternalZPiezoMon != None :
155             self.setExternalZPiezoMon(self.posMon_out2nm(self.curPosMon()))
156         if self.setExternalDeflection != None :
157             self.setExternalDeflection(self.def_in2V(self.curDef()))
158     def jumpToPos(self, pos) :
159         self._check_range(pos)
160         self.curOut[self.chan_info.zp_ind] = pos
161         self.curIn = self._jump.jumpToPos(self.curOut)
162         if self.setExternalZPiezo != None :
163             self.setExternalZPiezo(self.pos_out2nm(self.curPos()))
164         if TEXT_VERBOSE :
165             print "Jumped to"
166             self.pCurVals()
167         return self.curVals()
168     def ramp(self, out, freq) :
169         for pos in out :
170             self._check_range(pos)
171         out = self._ramp.ramp(out, freq)
172         self.curOut[self.chan_info.zp_ind] = out["Z piezo output"][-1]
173         self.curIn[self.chan_info.zp_mon_ind] = out["Z piezo input"][-1]
174         if USE_ABCD_DEFLECTION :
175             for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
176                 self.curIn[i] = out["Deflection segment"][i][-1]
177         else :
178             self.curIn[self.chan_info.def_ind] = out["Deflection input"][-1]
179
180         if self.setExternalZPiezo != None :
181             self.setExternalZPiezo(self.pos_out2nm(self.curPos()))
182         if self.setExternalZPiezoMon != None :
183             self.setExternalZPiezoMon(self.posMon_out2nm(self.curPosMon()))
184         if self.setExternalDeflection != None :
185             self.setExternalDeflection(self.def_in2V(self.curDef()))
186         if LOG_RAMPS :
187             log = data_logger.data_log(LOG_DIR, noclobber_logsubdir=False,
188                                        log_name="ramp")
189             if USE_ABCD_DEFLECTION :
190                 v = out.pop("Deflection segment")
191                 log.write_dict_of_arrays(out)
192                 out["Deflection segment"] = v
193             else :
194                 log.write_dict_of_arrays(out)
195         if TEXT_VERBOSE :
196             print "Ramped from (%g, %g) to (%g, %g) in %d points" \
197                 % (out["Z piezo output"][0], out["Deflection input"][0],
198                    out["Z piezo output"][-1], out["Deflection input"][-1],
199                    len(out["Z piezo output"]))
200         if PYLAB_INTERACTIVE_VERBOSE :
201             figure(BASE_FIG_NUM)
202             subplot(211)
203             title('ramp (def_in vs zp_out)')
204             timestamp = time.strftime('%H%M%S')
205             plot(out["Z piezo output"], out["Deflection input"], '.', label=timestamp)
206             subplot(212)
207             title('zp_in vs zp_out')
208             plot(out["Z piezo output"], out["Z piezo input"], '.', label=timestamp)
209             legend(loc='best')
210         return out
211     def _check_range(self, pos) :
212         if pos > self.zpMax :
213             raise outOfRange, "Z piezo pos = %d > %d = max" % (pos, self.zpMax)
214         if pos < self.zpMin :
215             raise outOfRange, "Z piezo pos = %d < %d = min" % (pos, self.zpMin)
216
217 # backends:
218  
219 class _z_piezo_jump :
220     def __init__(self, chan_info) :
221         self.chan_info = chan_info
222         self.reserved = False
223         self.AI = single_aio.AI(chan=self.chan_info.inChan)
224         self.AI.close()
225         self.AO = single_aio.AO(chan=self.chan_info.outChan)
226         self.AO.close()
227     def reserve(self) :
228         self.AI.open()
229         self.AO.open()
230         self.reserved = True
231     def release(self) :
232         self.AI.close()
233         self.AO.close()
234         self.reserved = False
235     def updateInputs(self) :
236         if self.reserved == False :
237             self.AI.open()
238             #self.reserve()
239             self.reserved = False
240         In = self.AI.read()
241         if self.reserved == False :
242             self.AI.close()
243             #self.release()
244         return In
245     def jumpToPos(self, out) :
246         if self.reserved == False :
247             self.reserve()
248             self.reserved = False # set to true inside reserve(), which we don't want
249         self.AO.write(out)
250         if self.reserved == False :
251             self.release()
252         return self.updateInputs()
253     def out_to_phys(self, output) :
254         return self.AO.comedi_to_phys(0, output)
255     def phys_to_out(self, physical) :
256         return self.AO.phys_to_comedi(0, physical)
257
258 class _z_piezo_ramp :
259     def __init__(self, chan_info) :
260         self.chan_info = chan_info
261         self.attempts=0
262         self.failures=0
263         self.verbose = True
264     def ramp(self, outArray, freq) :
265         # allocate and run the task
266         npoints = int(len(outArray)/self.chan_info.numOut)
267         in_data = array([0]*npoints*self.chan_info.numIn, dtype=uint16)
268         if type(outArray) != type(in_data) or outArray.dtype != uint16 :
269             out_data = array([0]*npoints*self.chan_info.numOut, dtype=uint16)
270             for i in range(npoints) :
271                 out_data[i] = outArray[i]
272         else :
273             out_data = outArray
274         if simult_aio.DONT_OUTPUT_LAST_SAMPLE_HACK == True:
275             # duplicate the last output point
276             npoints += 1
277             out_hack = array([0]*npoints*self.chan_info.numOut, dtype=uint16)
278             for i in range(npoints-1) :
279                 out_hack[i] = out_data[i]
280             out_hack[-1] = out_data[-1]
281             out_data = out_hack
282             in_data = array([0]*npoints*self.chan_info.numIn, dtype=uint16)
283
284         correlated = False
285         ramp={}
286         while not correlated :
287             AIO = simult_aio.AIO(in_chan=self.chan_info.inChan,
288                                  out_chan=self.chan_info.outChan)
289             if self.verbose :
290                 print "setup AIO"
291             AIO.setup(freq=freq, out_buffer=out_data)
292             if self.verbose :
293                 print "arm AIO"
294             AIO.arm()
295             if self.verbose :
296                 print "read AIO"
297             AIO.start_read(in_data)
298             if self.verbose :
299                 print "close AIO"
300             AIO.close()
301             if self.verbose :
302                 print "finished AIO"
303             self.attempts += 1
304             ramp["Z piezo output"] = out_data[self.chan_info.zp_ind::self.chan_info.numOut]
305             ramp["Z piezo input"] = in_data[self.chan_info.zp_mon_ind::self.chan_info.numIn]
306             ramp["Deflection input"] = in_data[self.chan_info.def_ind::self.chan_info.numIn]
307             failed = False
308             gradient, intercept, r_value, p_value, std_err = linregress(ramp["Z piezo output"],
309                                                                         ramp["Z piezo input"])
310             rnge = ramp["Z piezo output"].max()-ramp["Z piezo output"].min()
311             if gradient < .8 and rnge > 100 :
312                 if PYLAB_INTERACTIVE_VERBOSE :
313                     figure(BASE_FIG_NUM+3)
314                     subplot(211)
315                     title('ramp (def_in vs zp_out)')
316                     timestamp = time.strftime('%H%M%S')
317                     plot(ramp["Z piezo output"], ramp["Deflection input"], '.', label=timestamp)
318                     subplot(212)
319                     title('zp_in vs zp_out')
320                     plot(ramp["Z piezo output"], ramp["Z piezo input"], '.', label=timestamp)
321                     legend(loc='best')
322                 print "ramp failed slope (%g, %g), try again" % (gradient, rnge)
323                 failed = True
324             if not failed :
325                 if simult_aio.DONT_OUTPUT_LAST_SAMPLE_HACK == True:
326                     # remove the duplicated last output point
327                     out_data = out_data[:-self.chan_info.numOut]
328                     in_data = in_data[:-self.chan_info.numIn]
329                 ramp["Z piezo output"] = out_data[self.chan_info.zp_ind::self.chan_info.numOut]
330                 ramp["Z piezo input"] = in_data[self.chan_info.zp_mon_ind::self.chan_info.numIn]
331                 ramp["Deflection input"] = in_data[self.chan_info.def_ind::self.chan_info.numIn]
332
333                 if USE_ABCD_DEFLECTION :
334                     ramp["Deflection segment"] = []
335                     for c_ind in self.chan_info.def_ind :
336                         ramp["Deflection segment"].append(in_data[c_ind::self.chan_info.numIn])
337                     A = ramp["Deflection segment"][0].astype(float)
338                     B = ramp["Deflection segment"][1].astype(float)
339                     C = ramp["Deflection segment"][2].astype(float)
340                     D = ramp["Deflection segment"][3].astype(float)
341                     ramp["Deflection input"] = (((A+B)-(C+D))/(A+B+C+D) * 2**15).astype(uint16)
342                     ramp["Deflection input"] += 2**15 # HACK, comedi uses unsigned ints...
343                 else :
344                     ramp["Deflection input"] = in_data[self.chan_info.def_ind::self.chan_info.numIn]
345                 correlated = True
346                 if PYLAB_INTERACTIVE_VERBOSE :
347                     figure(BASE_FIG_NUM+1)
348                     timestamp = time.strftime('%H%M%S')
349                     plot(ramp["Z piezo output"], ramp["Z piezo input"], '.', label=timestamp)
350                     title('goods')
351                     legend(loc='best')
352             else :
353                 self.failures += 1
354                 if PYLAB_INTERACTIVE_VERBOSE :
355                     figure(BASE_FIG_NUM+2)
356                     timestamp = time.strftime('%H%M%S')
357                     plot(ramp["Z piezo output"], ramp["Z piezo input"], '.', label=timestamp)
358                     legend(loc='best')
359                     title('bads')
360
361         #AIO = simAIO.simAIOobj(npoints,
362         #                       freq,
363         #                       self.chan_info.inChan,
364         #                       self.chan_info.outChan)
365         #err = AIO.configure()
366         #err = AIO.commit()
367         #err = AIO.write(outArray)
368         #err = AIO.run()
369         #(err, inArray) = AIO.read()
370         #if err != 0 :
371         #    raise error, "Error in simAIO"
372         #if len(inArray)*self.chan_info.numOut != len(outArray)*self.chan_info.numIn :
373         #    raise error, "Input array of wrong length"
374         #
375         #ramp={}
376         #ramp["Z piezo output"] = outArray[self.chan_info.zp_ind::self.chan_info.numOut]
377         #ramp["Z piezo input"] = inArray[self.chan_info.zp_mon_ind::self.chan_info.numIn]
378         #ramp["Deflection input"] = inArray[self.chan_info.def_ind::self.chan_info.numIn]
379         
380         # task automatically freed as AIO goes out of scope
381         return ramp
382
383 class _chan_info :
384     def __init__(self, zp_chan, zp_mon_chan, def_chan) :
385         self.inChan = []
386         if USE_ABCD_DEFLECTION :
387             self.def_ind = [] # index in inChan array
388             i=0
389             for chan in def_chan :
390                 self.inChan.append(chan)
391                 self.def_ind.append(i)
392                 i += 1
393         else :
394             self.inChan.append(def_chan)
395             self.def_ind = 0 # index in inChan array
396         self.inChan.append(zp_mon_chan)
397         self.zp_mon_ind = zp_mon_chan
398         self.numIn = len(self.inChan)
399         
400         self.outChan = [zp_chan]
401         self.zp_ind = 0 # index in outChan array
402         self.numOut = len(self.outChan)
403         
404
405 def test() :
406     print "test z_piezo()"
407     zp = z_piezo()
408     zp.verbose = True
409     zp.jumpToPos(zp.pos_nm2out(0))
410     if zp.verbose :
411         zp.pCurVals()
412     curPos = zp.curPos()
413     curPosNm = zp.pos_out2nm(curPos)
414     curPos2 = zp.pos_nm2out(curPosNm)
415     if curPos != curPos2 :
416         raise error, "Conversion %d -> %g -> %d not round trip" % (curPos, curPosNm, curPos2)
417     ramp_positions = linspace(zp.curPos(), zp.pos_nm2out(100), 20)
418     out = zp.ramp(ramp_positions, 10000)
419     ramp_positions = linspace(zp.curPos(), zp.pos_nm2out(0), 20)
420     out2 = zp.ramp(ramp_positions, 10000)
421
422 if __name__ == "__main__" :
423     test()