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