1 # Copyright (C) 2011-2012 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 under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
10 # pypiezo is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # pypiezo. If not, see <http://www.gnu.org/licenses/>.
17 "Control of a piezo-based atomic force microscope."
21 import numpy as _numpy
24 import matplotlib as _matplotlib
25 import matplotlib.pyplot as _matplotlib_pyplot
26 except (ImportError, RuntimeError), e:
28 _matplotlib_import_error = e
30 from . import LOG as _LOG
31 from . import base as _base
32 from . import package_config as _package_config
33 from . import surface as _surface
34 from . import wiggle as _wiggle
37 class AFMPiezo (_base.Piezo):
38 """A piezo-controlled atomic force microscope.
40 This particular class expects a single input channel for measuring
41 deflection. Other subclasses provide support for multi-segment
42 deflection measurements.
44 >>> from pprint import pprint
45 >>> from pycomedi.device import Device
46 >>> from pycomedi.subdevice import StreamingSubdevice
47 >>> from pycomedi.channel import AnalogChannel
48 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
49 >>> from . import config
50 >>> from . import surface
52 >>> d = Device('/dev/comedi0')
55 >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
56 ... factory=StreamingSubdevice)
57 >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
58 ... factory=StreamingSubdevice)
60 >>> axis_channel = s_out.channel(
61 ... 0, factory=AnalogChannel, aref=AREF.ground)
62 >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
63 >>> for chan in [axis_channel, input_channel]:
64 ... chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
66 We set the minimum voltage for the `z` axis to -9 (a volt above
67 the minimum possible voltage) to help with testing
68 `.get_surface_position`. Without this minimum voltage, small
69 calibration errors could lead to a railed -10 V input for the
70 first few surface approaching steps, which could lead to an
71 `EdgeKink` error instead of a `FlatFit` error.
73 >>> axis_config = config.AxisConfig()
74 >>> axis_config.update(
75 ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
76 >>> axis_config['channel'] = config.OutputChannelConfig()
77 >>> axis_config['channel']['name'] = 'z'
78 >>> input_config = config.InputChannelConfig()
79 >>> input_config['name'] = 'deflection'
81 >>> a = _base.PiezoAxis(config=axis_config, axis_channel=axis_channel)
84 >>> c = _base.InputChannel(config=input_config, channel=input_channel)
87 >>> p = AFMPiezo(axes=[a], inputs=[c], name='Molly')
89 >>> deflection = p.read_deflection()
90 >>> deflection # doctest: +SKIP
92 >>> p.deflection_dtype()
95 We need to know where we are before we can move somewhere
98 >>> pos = _base.convert_volts_to_bits(p.config.select_config(
99 ... 'axes', 'z', get_attribute=_base.get_axis_name)['channel'], 0)
102 Usually `.move_to_pos_or_def` is used to approach the surface, but
103 for testing we assume the z output channel is connected directly
104 into the deflection input channel.
106 >>> target_pos = _base.convert_volts_to_bits(
107 ... p.config.select_config('axes', 'z',
108 ... get_attribute=_base.get_axis_name)['channel'], 2)
109 >>> step = int((target_pos - pos)/5)
110 >>> target_def = _base.convert_volts_to_bits(
111 ... p.config.select_config('inputs', 'deflection'), 3)
112 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
113 ... return_data=True)
114 >>> p.last_output == {'z': int(target_pos)}
116 >>> pprint(data) # doctest: +SKIP
118 array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
120 array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
122 That was a working position-limited approach. Now move back to
123 the center and try a deflection-limited approach.
126 >>> target_def = _base.convert_volts_to_bits(
127 ... p.config.select_config('inputs', 'deflection'), 1)
128 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
129 ... return_data=True)
130 >>> print (p.last_output['z'] < int(target_pos))
132 >>> pprint(data) # doctest: +SKIP
133 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
134 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
136 >>> wiggle_config = config.WiggleConfig()
137 >>> wiggle_config['offset'] = p.last_output['z']
138 >>> wiggle_config['wavelength'] = 650e-9
139 >>> p.wiggle_for_interference(config=wiggle_config,
140 ... keypress_test_mode=True)
141 Press any key to continue
144 ... p.get_surface_position('z', max_deflection=target_def)
145 ... except surface.FlatFit, e:
146 ... print 'got FlatFit'
148 >>> print e # doctest: +SKIP
149 slopes not sufficiently different: 1.0021 and 1.0021
150 >>> abs(e.right_slope-1) < 0.1
152 >>> abs(e.left_slope-1) < 0.1
157 def _deflection_channel(self):
158 return self.channel_by_name(name='deflection', direction='input')
160 def read_deflection(self):
161 """Return sensor deflection in bits.
163 TODO: explain how bit <-> volt conversion will work for this
166 return self._deflection_channel().data_read()
168 def deflection_dtype(self):
169 "Return a Numpy dtype suitable for deflection bit values."
170 return self._deflection_channel().subdevice.get_dtype()
172 def move_to_pos_or_def(self, axis_name, position=None, deflection=None,
173 step=1, return_data=False, pre_move_steps=0,
178 number of 'null' steps to take before moving (confirming a
179 stable input deflection).
181 The target step frequency in hertz. If `Null`, go as fast
182 as possible. Note that this is software timing, so it
183 should not be relied upon for precise results.
185 if position is None and deflection is None:
186 raise ValueError('must specify position, deflection, or both')
188 if return_data or _package_config['matplotlib']:
194 # default to the extreme value in the step direction
196 axis = self.axis_by_name(axis_name)
197 position = axis.axis_channel.get_maxdata()
200 elif deflection is None:
201 # default to the extreme value
202 channel = self._deflection_channel(self)
203 deflection = channel.get_maxdata()
206 raise ValueError('must have non-zero step size')
207 elif step < 0 and position > self.last_output[axis_name]:
209 elif step > 0 and position < self.last_output[axis_name]:
213 'move to position %d or deflection %g on axis %s in steps of %d'
214 % (position, deflection, axis_name, step))
215 _LOG.debug(log_string)
216 current_deflection = self.read_deflection()
217 log_string = 'current position %d and deflection %g' % (
218 self.last_output[axis_name], current_deflection)
219 _LOG.debug(log_string)
222 def_array=[current_deflection]
223 pos_array=[self.last_output[axis_name]]
224 for i in range(pre_move_steps):
225 self.jump(axis_name, piezo.last_output[axis_name])
226 delection = self.read_deflection()
228 def_array.append(current_deflection)
229 pos_array.append(self.last_output[axis_name])
230 if frequency is not None:
231 time_step = 1./frequency
232 next_time = _time.time() + time_step
233 # step in until we hit our target position or exceed our target deflection
234 while (self.last_output[axis_name] != position and
235 current_deflection < deflection):
236 dist_to = position - self.last_output[axis_name]
237 if abs(dist_to) < abs(step):
240 jump_to = self.last_output[axis_name] + step
241 self.jump(axis_name, jump_to)
242 current_deflection = self.read_deflection()
244 'current z piezo position %6d, current deflection %6d'
245 % (current_deflection, self.last_output[axis_name]))
246 _LOG.debug(log_string)
248 def_array.append(current_deflection)
249 pos_array.append(self.last_output[axis_name])
250 if frequency is not None:
253 _time.sleep(next_time - now)
254 next_time += time_step
257 'move to position %d or deflection %g on axis %s complete'
258 % (position, deflection, axis_name))
259 _LOG.debug(log_string)
260 log_string = 'current position %d and deflection %g' % (
261 self.last_output[axis_name], current_deflection)
262 _LOG.debug(log_string)
263 if _package_config['matplotlib']:
265 raise _matplotlib_import_error
266 figure = _matplotlib_pyplot.figure()
267 axes = figure.add_subplot(1, 1, 1)
269 timestamp = _time.strftime('%H%M%S')
270 axes.set_title('step approach %s' % timestamp)
271 axes.plot(pos_array, def_array, '.', label=timestamp)
272 #_pylab.legend(loc='best')
277 axis_name:_numpy.array(
278 pos_array, dtype=self.channel_dtype(
279 axis_name, direction='output')),
280 'deflection':_numpy.array(
281 def_array, dtype=self.deflection_dtype()),
285 wiggle_for_interference = _wiggle.wiggle_for_interference
286 get_surface_position = _surface.get_surface_position
290 # if USE_ABCD_DEFLECTION :
291 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
292 # self.curIn[i] = out["Deflection segment"][i][-1]
294 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
297 #class FourSegmentAFM (AFM):
298 # def read_deflection(self):
299 # "Return sensor deflection in bits."
300 # A = int(self.curIn[self.chan_info.def_ind[0]])
301 # B = int(self.curIn[self.chan_info.def_ind[1]])
302 # C = int(self.curIn[self.chan_info.def_ind[2]])
303 # D = int(self.curIn[self.chan_info.def_ind[3]])
304 # df = float((A+B)-(C+D))/(A+B+C+D)
305 # dfout = int(df * 2**15) + 2**15
307 # print "Current deflection %d (%d, %d, %d, %d)" \
308 # % (dfout, A, B, C, D)
312 #def test_smoothness(zp, plotVerbose=True):
315 # setpoint = zp.def_V2in(3)
318 # outarray = linspace(posB, posA, 1000)
321 # curVals = zp.jumpToPos(posA)
322 # zp.pCurVals(curVals)
323 # _sleep(1) # let jitters die down
324 # for i in range(10):
325 # print "ramp %d to %d" % (zp.curPos(), posB)
326 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
327 # return_data = True)
328 # indata.append(data)
329 # out = zp.ramp(outarray, outfreq)
330 # outdata.append(out)
332 # from pylab import figure, plot, title, legend, hold, subplot
333 # if PYLAB_VERBOSE or plotVerbose:
335 # _pylab.figure(BASE_FIG_NUM+4)
336 # for i in range(10):
337 # _pylab.plot(indata[i]['z'],
338 # indata[i]['deflection'], '+--', label='in')
339 # _pylab.plot(outdata[i]['z'],
340 # outdata[i]['deflection'], '.-', label='out')
341 # _pylab.title('test smoothness (step in, ramp out)')
342 # #_pylab.legend(loc='best')
346 # zp = z_piezo.z_piezo()
347 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
349 # zp.pCurVals(curVals)
350 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
352 # print "Surface at %g nm", pos
354 # if PYLAB_VERBOSE and _final_flush_plot != None:
355 # _final_flush_plot()