1 # Copyright (C) 2008-2011 W. Trevor King <wking@drexel.edu>
3 # This file is part of pypiezo.
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.
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.
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/>.
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.
24 There are a few globals controlling the behavior of the entire module.
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)
47 USE_ABCD_DEFLECTION = False
49 PYLAB_INTERACTIVE_VERBOSE = False
52 LOG_DIR = '${DEFAULT}/z_piezo'
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
62 DEFAULT_ZP_MON_CHAN = 1
64 if USE_ABCD_DEFLECTION :
65 DEFAULT_DEF_CHAN = (2,3,4,5)
66 # The 4 photodiode segments are
69 # looking in along the laser
71 class NoComedi (ImportError):
72 "Missing comedi. Don't initialize a z_piezo instance."
76 import pycomedi.single_aio as single_aio
77 import pycomedi.simult_aio as simult_aio
79 except ImportError, e:
82 from numpy import array, arange, ones, zeros, linspace, uint16, float, sin, pi
83 from scipy.stats import linregress
86 if PYLAB_INTERACTIVE_VERBOSE :
87 from pylab import figure, plot, title, legend, hold, subplot
88 import time # for timestamping lines on plots
90 class error (Exception) :
92 class outOfRange (error) :
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:
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)
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
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])
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
133 print "Current deflection %d (%d, %d, %d, %d)" \
134 % (dfout, A, B, C, D)
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)
160 return {'Z piezo output': self.curPos(),
161 'Deflection input':self.curDef(),
162 'Z piezo input':self.curPosMon()}
163 def pCurVals(self, 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()))
184 return self.curVals()
185 def ramp(self, out, freq) :
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]
195 self.curIn[self.chan_info.def_ind] = out["Deflection input"][-1]
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()))
204 log = data_logger.data_log(LOG_DIR, noclobber_logsubdir=False,
206 if USE_ABCD_DEFLECTION :
207 v = out.pop("Deflection segment")
208 log.write_dict_of_arrays(out)
209 out["Deflection segment"] = v
211 log.write_dict_of_arrays(out)
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 :
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)
224 title('zp_in vs zp_out')
225 plot(out["Z piezo output"], out["Z piezo input"], '.', label=timestamp)
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)
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)
242 self.AO = single_aio.AO(chan=self.chan_info.outChan)
251 self.reserved = False
252 def updateInputs(self) :
253 if self.reserved == False :
256 self.reserved = False
258 if self.reserved == False :
262 def jumpToPos(self, out) :
263 if self.reserved == False :
265 self.reserved = False # set to true inside reserve(), which we don't want
267 if self.reserved == False :
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)
275 class _z_piezo_ramp :
276 def __init__(self, chan_info) :
277 self.chan_info = chan_info
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]
291 if simult_aio.DONT_OUTPUT_LAST_SAMPLE_HACK == True:
292 # duplicate the last output point
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]
299 in_data = array([0]*npoints*self.chan_info.numIn, dtype=uint16)
303 while not correlated :
304 AIO = simult_aio.AIO(in_chan=self.chan_info.inChan,
305 out_chan=self.chan_info.outChan)
308 AIO.setup(freq=freq, out_buffer=out_data)
314 AIO.start_read(in_data)
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]
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)
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)
336 title('zp_in vs zp_out')
337 plot(ramp["Z piezo output"], ramp["Z piezo input"], '.', label=timestamp)
339 print "ramp failed slope (%g, %g), try again" % (gradient, rnge)
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]
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...
361 ramp["Deflection input"] = in_data[self.chan_info.def_ind::self.chan_info.numIn]
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)
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)
378 #AIO = simAIO.simAIOobj(npoints,
380 # self.chan_info.inChan,
381 # self.chan_info.outChan)
382 #err = AIO.configure()
384 #err = AIO.write(outArray)
386 #(err, inArray) = AIO.read()
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"
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]
397 # task automatically freed as AIO goes out of scope
401 def __init__(self, zp_chan, zp_mon_chan, def_chan) :
403 if USE_ABCD_DEFLECTION :
404 self.def_ind = [] # index in inChan array
406 for chan in def_chan :
407 self.inChan.append(chan)
408 self.def_ind.append(i)
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)
417 self.outChan = [zp_chan]
418 self.zp_ind = 0 # index in outChan array
419 self.numOut = len(self.outChan)
423 print "test z_piezo()"
426 zp.jumpToPos(zp.pos_nm2out(0))
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)
439 if __name__ == "__main__" :