1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
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.constant import AREF
46 >>> from . import config
47 >>> from . import surface
51 >>> piezo_config = config.PiezoConfig()
52 >>> piezo_config['name'] = 'Molly'
53 >>> piezo_config['axes'] = [config.AxisConfig()]
54 >>> piezo_config['axes'][0]['channel'] = config.OutputChannelConfig()
55 >>> piezo_config['axes'][0]['channel']['analog-reference'] = AREF.ground
56 >>> piezo_config['axes'][0]['channel']['name'] = 'z'
57 >>> piezo_config['inputs'] = [config.InputChannelConfig()]
58 >>> piezo_config['inputs'][0]['analog-reference'] = AREF.diff
59 >>> piezo_config['inputs'][0]['name'] = 'deflection'
61 We set the minimum voltage for the `z` axis to -9 (a volt above
62 the minimum possible voltage) to help with testing
63 `.get_surface_position`. Without this minimum voltage, small
64 calibration errors could lead to a railed -10 V input for the
65 first few surface approaching steps, which could lead to an
66 `EdgeKink` error instead of a `FlatFit` error.
68 >>> piezo_config['axes'][0].update(
69 ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
71 >>> p = AFMPiezo(config=piezo_config)
72 >>> p.load_from_config(devices=devices)
75 >>> deflection = p.read_deflection()
76 >>> deflection # doctest: +SKIP
78 >>> p.deflection_dtype()
81 We need to know where we are before we can move somewhere
84 >>> zeros = p.zero(axis_names=['z'])
87 Usually `.move_to_pos_or_def` is used to approach the surface, but
88 for testing we assume the z output channel is connected directly
89 into the deflection input channel.
91 >>> target_pos = _base.convert_volts_to_bits(
92 ... p.config.select_config('axes', 'z',
93 ... get_attribute=_base.get_axis_name)['channel'], 2)
94 >>> step = int((target_pos - pos)/5)
95 >>> target_def = _base.convert_volts_to_bits(
96 ... p.config.select_config('inputs', 'deflection'), 3)
97 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
99 >>> p.last_output == {'z': int(target_pos)}
101 >>> pprint(data) # doctest: +SKIP
103 array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
105 array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
107 That was a working position-limited approach. Now move back to
108 the center and try a deflection-limited approach.
111 >>> target_def = _base.convert_volts_to_bits(
112 ... p.config.select_config('inputs', 'deflection'), 1)
113 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
114 ... return_data=True)
115 >>> print (p.last_output['z'] < int(target_pos))
117 >>> pprint(data) # doctest: +SKIP
118 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
119 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
121 >>> wiggle_config = config.WiggleConfig()
122 >>> wiggle_config['offset'] = p.last_output['z']
123 >>> wiggle_config['wavelength'] = 650e-9
124 >>> p.wiggle_for_interference(config=wiggle_config,
125 ... keypress_test_mode=True)
126 Press any key to continue
129 ... p.get_surface_position('z', max_deflection=target_def)
130 ... except surface.FlatFit, e:
131 ... print 'got FlatFit'
133 >>> print e # doctest: +SKIP
134 slopes not sufficiently different: 1.0021 and 1.0021
135 >>> abs(e.right_slope-1) < 0.1
137 >>> abs(e.left_slope-1) < 0.1
140 >>> for device in devices:
143 def _deflection_channel(self):
144 return self.channel_by_name(name='deflection', direction='input')
146 def read_deflection(self):
147 """Return sensor deflection in bits.
149 TODO: explain how bit <-> volt conversion will work for this
152 return self._deflection_channel().data_read()
154 def deflection_dtype(self):
155 "Return a Numpy dtype suitable for deflection bit values."
156 return self._deflection_channel().subdevice.get_dtype()
158 def move_to_pos_or_def(self, axis_name, position=None, deflection=None,
159 step=1, return_data=False, pre_move_steps=0,
164 number of 'null' steps to take before moving (confirming a
165 stable input deflection).
167 The target step frequency in hertz. If `Null`, go as fast
168 as possible. Note that this is software timing, so it
169 should not be relied upon for precise results.
171 if position is None and deflection is None:
172 raise ValueError('must specify position, deflection, or both')
174 if return_data or _package_config['matplotlib']:
180 # default to the extreme value in the step direction
182 axis = self.axis_by_name(axis_name)
183 position = axis.axis_channel.get_maxdata()
186 elif deflection is None:
187 # default to the extreme value
188 channel = self._deflection_channel(self)
189 deflection = channel.get_maxdata()
190 position = int(position) # round down to nearest integer
193 raise ValueError('must have non-zero step size')
194 elif step < 0 and position > self.last_output[axis_name]:
196 elif step > 0 and position < self.last_output[axis_name]:
200 'move to position %d or deflection %g on axis %s in steps of %d'
201 % (position, deflection, axis_name, step))
202 _LOG.debug(log_string)
203 current_deflection = self.read_deflection()
204 log_string = 'current position %d and deflection %g' % (
205 self.last_output[axis_name], current_deflection)
206 _LOG.debug(log_string)
209 def_array=[current_deflection]
210 pos_array=[self.last_output[axis_name]]
211 for i in range(pre_move_steps):
212 self.jump(axis_name, piezo.last_output[axis_name])
213 current_deflection = self.read_deflection()
215 def_array.append(current_deflection)
216 pos_array.append(self.last_output[axis_name])
217 if frequency is not None:
218 time_step = 1./frequency
219 next_time = _time.time() + time_step
220 # step in until we hit our target position or exceed our target deflection
221 while (self.last_output[axis_name] != position and
222 current_deflection < deflection):
223 dist_to = position - self.last_output[axis_name]
224 if abs(dist_to) < abs(step):
227 jump_to = self.last_output[axis_name] + step
228 self.jump(axis_name, jump_to)
229 current_deflection = self.read_deflection()
231 ('current z piezo position {} (target {}), '
232 'current deflection {} (target {})').format(
233 self.last_output[axis_name], position,
234 current_deflection, deflection))
235 _LOG.debug(log_string)
237 def_array.append(current_deflection)
238 pos_array.append(self.last_output[axis_name])
239 if frequency is not None:
242 _time.sleep(next_time - now)
243 next_time += time_step
246 'move to position %d or deflection %g on axis %s complete'
247 % (position, deflection, axis_name))
248 _LOG.debug(log_string)
249 log_string = 'current position %d and deflection %g' % (
250 self.last_output[axis_name], current_deflection)
251 _LOG.debug(log_string)
252 if _package_config['matplotlib']:
254 raise _matplotlib_import_error
255 figure = _matplotlib_pyplot.figure()
256 axes = figure.add_subplot(1, 1, 1)
258 timestamp = _time.strftime('%H%M%S')
259 axes.set_title('step approach %s' % timestamp)
260 axes.plot(pos_array, def_array, '.', label=timestamp)
261 #_pylab.legend(loc='best')
264 if not _matplotlib.is_interactive():
265 _matplotlib_pyplot.show()
269 axis_name:_numpy.array(
270 pos_array, dtype=self.channel_dtype(
271 axis_name, direction='output')),
272 'deflection':_numpy.array(
273 def_array, dtype=self.deflection_dtype()),
277 wiggle_for_interference = _wiggle.wiggle_for_interference
278 get_surface_position = _surface.get_surface_position
282 # if USE_ABCD_DEFLECTION :
283 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
284 # self.curIn[i] = out["Deflection segment"][i][-1]
286 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
289 #class FourSegmentAFM (AFM):
290 # def read_deflection(self):
291 # "Return sensor deflection in bits."
292 # A = int(self.curIn[self.chan_info.def_ind[0]])
293 # B = int(self.curIn[self.chan_info.def_ind[1]])
294 # C = int(self.curIn[self.chan_info.def_ind[2]])
295 # D = int(self.curIn[self.chan_info.def_ind[3]])
296 # df = float((A+B)-(C+D))/(A+B+C+D)
297 # dfout = int(df * 2**15) + 2**15
299 # print "Current deflection %d (%d, %d, %d, %d)" \
300 # % (dfout, A, B, C, D)
304 #def test_smoothness(zp, plotVerbose=True):
307 # setpoint = zp.def_V2in(3)
310 # outarray = linspace(posB, posA, 1000)
313 # curVals = zp.jumpToPos(posA)
314 # zp.pCurVals(curVals)
315 # _sleep(1) # let jitters die down
316 # for i in range(10):
317 # print "ramp %d to %d" % (zp.curPos(), posB)
318 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
319 # return_data = True)
320 # indata.append(data)
321 # out = zp.ramp(outarray, outfreq)
322 # outdata.append(out)
324 # from pylab import figure, plot, title, legend, hold, subplot
325 # if PYLAB_VERBOSE or plotVerbose:
327 # _pylab.figure(BASE_FIG_NUM+4)
328 # for i in range(10):
329 # _pylab.plot(indata[i]['z'],
330 # indata[i]['deflection'], '+--', label='in')
331 # _pylab.plot(outdata[i]['z'],
332 # outdata[i]['deflection'], '.-', label='out')
333 # _pylab.title('test smoothness (step in, ramp out)')
334 # #_pylab.legend(loc='best')
338 # zp = z_piezo.z_piezo()
339 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
341 # zp.pCurVals(curVals)
342 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
344 # print "Surface at %g nm", pos
346 # if PYLAB_VERBOSE and _final_flush_plot != None:
347 # _final_flush_plot()