Move calibcant.calibrate.move_far_from_surface -> pyafm.afm.AFM.move_away_from_surface.
[pyafm.git] / pyafm / afm.py
index 69d22c93e73bbb0b91901b61ebc0d97eadc625d2..3093a07997cc0d47890cd36b0882aed80c798e7a 100644 (file)
@@ -23,6 +23,7 @@ for controlling the piezo (`pypiezo`) and stepper (`stepper`), this
 module only contains methods that require the capabilities of both.
 """
 
+from pypiezo.afm import AFMPiezo as _AFMPiezo
 from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters
 from pypiezo.base import convert_meters_to_bits as _convert_meters_to_bits
 from pypiezo.base import convert_volts_to_bits as _convert_volts_to_bits
@@ -30,6 +31,8 @@ from pypiezo.surface import FlatFit as _FlatFit
 from pypiezo.surface import SurfaceError as _SurfaceError
 
 from . import LOG as _LOG
+from .stepper import Stepper as _Stepper
+from .temperature import Temperature as _Temperature
 
 
 class AFM (object):
@@ -46,12 +49,264 @@ class AFM (object):
         Coarse positioning.
     temperature | temperature.Controller instance or None
         Optional temperature monitoring and control.
+
+    >>> import os
+    >>> import tempfile
+    >>> from pycomedi import constant
+    >>> import pypiezo.config
+    >>> import pyafm.config
+    >>> import pyafm.storage
+    >>> from h5config.storage.hdf5 import pprint_HDF5
+
+    >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pyafm-')
+    >>> os.close(fd)
+
+    >>> devices = []
+
+    >>> config = pyafm.config.AFMConfig()
+    >>> config['piezo'] = pypiezo.config.PiezoConfig()
+    >>> config['piezo']['name'] = 'test piezo'
+    >>> config['piezo']['axes'] = [pypiezo.config.AxisConfig()]
+    >>> config['piezo']['axes'][0]['channel'] = (
+    ...     pypiezo.config.OutputChannelConfig())
+    >>> config['piezo']['axes'][0]['channel']['name'] = 'z'
+    >>> config['piezo']['inputs'] = [pypiezo.config.InputChannelConfig()]
+    >>> config['piezo']['inputs'][0]['name'] = 'deflection'
+    >>> config['stepper'] = pyafm.config.StepperConfig()
+    >>> config['stepper']['port'] = pyafm.config.DigitalPortConfig()
+    >>> config['stepper']['port']['channels'] = [1, 2, 3, 4]
+    >>> config['stepper']['port']['direction'] = constant.IO_DIRECTION.output
+    >>> config['stepper']['port']['name'] = 'stepper port'
+    >>> config['stepper']['name'] = 'test stepper'
+    >>> config['temperature'] = pyafm.config.TemperatureConfig()
+    >>> config['temperature']['name'] = 'test temperature'
+
+    >>> afm = AFM(config=config, devices=devices)
+    >>> afm.setup_config()
+
+    >>> afm.get_temperature()  # doctest: +SKIP
+    297.37
+
+    >>> print(afm.config.dump())  # doctest: +REPORT_UDIFF
+    name: 
+    main-axis: 
+    piezo:
+      name: test piezo
+      axes:
+        0:
+          gain: 1.0
+          sensitivity: 1.0
+          minimum: -10.0
+          maximum: 10.0
+          channel:
+            name: z
+            device: /dev/comedi0
+            subdevice: 1
+            channel: 0
+            maxdata: 65535
+            range: 0
+            analog-reference: ground
+            conversion-coefficients: -10.0,0.000305180437934
+            conversion-origin: 0.0
+            inverse-conversion-coefficients: 0.0,3276.75
+            inverse-conversion-origin: -10.0
+          monitor: 
+      inputs:
+        0:
+          name: deflection
+          device: /dev/comedi0
+          subdevice: 0
+          channel: 0
+          maxdata: 65535
+          range: 0
+          analog-reference: ground
+          conversion-coefficients: -10.0,0.000305180437934
+          conversion-origin: 0.0
+          inverse-conversion-coefficients: 0.0,3276.75
+          inverse-conversion-origin: -10.0
+    stepper:
+      name: test stepper
+      full-step: yes
+      logic: yes
+      delay: 0.01
+      step-size: 1.7e-07
+      backlash: 100
+      port:
+        name: stepper port
+        device: /dev/comedi0
+        subdevice: 2
+        subdevice-type: dio
+        channels: 1,2,3,4
+        direction: output
+    temperature:
+      name: test temperature
+      units: Celsius
+      controller: 1
+      device: /dev/ttyS0
+      baudrate: 9600
+      max-current: 0.0
+    fallback-temperature: 295.15
+    far: 3e-05
+
+    >>> pyafm.storage.save_afm(afm=afm, filename=filename)
+    >>> pprint_HDF5(filename=filename)  # doctest: +REPORT_UDIFF
+    /
+      <HDF5 dataset "fallback-temperature": shape (), type "<f8">
+        295.15
+      <HDF5 dataset "far": shape (), type "<f8">
+        3e-05
+      <HDF5 dataset "main-axis": shape (), type "|S1">
+    <BLANKLINE>
+      <HDF5 dataset "name": shape (), type "|S1">
+    <BLANKLINE>
+      /piezo
+        /piezo/axes
+          /piezo/axes/0
+            /piezo/axes/0/channel
+              <HDF5 dataset "analog-reference": shape (), type "|S6">
+                ground
+              <HDF5 dataset "channel": shape (), type "<i4">
+                0
+              <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
+                [ -1.00000000e+01   3.05180438e-04]
+              <HDF5 dataset "conversion-origin": shape (), type "<f8">
+                0.0
+              <HDF5 dataset "device": shape (), type "|S12">
+                /dev/comedi0
+              <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
+                [    0.    3276.75]
+              <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
+                -10.0
+              <HDF5 dataset "maxdata": shape (), type "<i8">
+                65535
+              <HDF5 dataset "name": shape (), type "|S1">
+                z
+              <HDF5 dataset "range": shape (), type "<i4">
+                0
+              <HDF5 dataset "subdevice": shape (), type "<i4">
+                1
+            <HDF5 dataset "gain": shape (), type "<f8">
+              1.0
+            <HDF5 dataset "maximum": shape (), type "<f8">
+              10.0
+            <HDF5 dataset "minimum": shape (), type "<f8">
+              -10.0
+            <HDF5 dataset "monitor": shape (), type "|S1">
+    <BLANKLINE>
+            <HDF5 dataset "sensitivity": shape (), type "<f8">
+              1.0
+        /piezo/inputs
+          /piezo/inputs/0
+            <HDF5 dataset "analog-reference": shape (), type "|S6">
+              ground
+            <HDF5 dataset "channel": shape (), type "<i4">
+              0
+            <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
+              [ -1.00000000e+01   3.05180438e-04]
+            <HDF5 dataset "conversion-origin": shape (), type "<f8">
+              0.0
+            <HDF5 dataset "device": shape (), type "|S12">
+              /dev/comedi0
+            <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
+              [    0.    3276.75]
+            <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
+              -10.0
+            <HDF5 dataset "maxdata": shape (), type "<i8">
+              65535
+            <HDF5 dataset "name": shape (), type "|S10">
+              deflection
+            <HDF5 dataset "range": shape (), type "<i4">
+              0
+            <HDF5 dataset "subdevice": shape (), type "<i4">
+              0
+        <HDF5 dataset "name": shape (), type "|S10">
+          test piezo
+      /stepper
+        <HDF5 dataset "backlash": shape (), type "<i4">
+          100
+        <HDF5 dataset "delay": shape (), type "<f8">
+          0.01
+        <HDF5 dataset "full-step": shape (), type "|b1">
+          True
+        <HDF5 dataset "logic": shape (), type "|b1">
+          True
+        <HDF5 dataset "name": shape (), type "|S12">
+          test stepper
+        /stepper/port
+          <HDF5 dataset "channels": shape (4,), type "<i4">
+            [1 2 3 4]
+          <HDF5 dataset "device": shape (), type "|S12">
+            /dev/comedi0
+          <HDF5 dataset "direction": shape (), type "|S6">
+            output
+          <HDF5 dataset "name": shape (), type "|S12">
+            stepper port
+          <HDF5 dataset "subdevice": shape (), type "<i4">
+            2
+          <HDF5 dataset "subdevice-type": shape (), type "|S3">
+            dio
+        <HDF5 dataset "step-size": shape (), type "<f8">
+          1.7e-07
+      /temperature
+        <HDF5 dataset "baudrate": shape (), type "<i4">
+          9600
+        <HDF5 dataset "controller": shape (), type "<i4">
+          1
+        <HDF5 dataset "device": shape (), type "|S10">
+          /dev/ttyS0
+        <HDF5 dataset "max-current": shape (), type "<f8">
+          0.0
+        <HDF5 dataset "name": shape (), type "|S16">
+          test temperature
+        <HDF5 dataset "units": shape (), type "|S7">
+          Celsius
+    >>> afm2 = pyafm.storage.load_afm(filename=filename, devices=devices)
+
+    >>> afm2.get_temperature()  # doctest: +SKIP
+    297.37
+
+    It's hard to test anything else without pugging into an actual AFM.
+
+    >>> for device in devices:
+    ...     device.close()
+
+    Cleanup our temporary config file.
+
+    >>> os.remove(filename)
     """
-    def __init__(self, piezo, stepper, temperature=None, axis_name='z'):
+    def __init__(self, config, piezo=None, stepper=None, temperature=None,
+                 devices=None):
+        self.config = config
         self.piezo = piezo
         self.stepper = stepper
         self.temperature = temperature
-        self.axis_name = axis_name
+        self.load_from_config(devices=devices)
+
+    def load_from_config(self, devices):
+        c = self.config  # reduce verbosity
+        if self.piezo is None and c['piezo']:
+            self.piezo = _AFMPiezo(config=c['piezo'], devices=devices)
+        if self.stepper is None and c['stepper']:
+            self.stepper = _Stepper(config=c['stepper'], devices=devices)
+        if self.temperature is None and c['temperature']:
+            self.temperature = _Temperature(config=c['temperature'])
+
+    def setup_config(self):
+        if self.piezo:
+            self.piezo.setup_config()
+            self.config['piezo'] = self.piezo.config
+        else:
+            self.config['piezo'] = None
+        if self.stepper:
+            self.stepper.setup_config()
+            self.config['stepper'] = self.stepper.config
+        else:
+            self.config['stepper'] = None
+        if self.temperature:
+            self.temperature.setup_config()
+            self.config['temperature'] = self.temperature.config
+        else:
+            self.config['temperature'] = None
 
     def get_temperature(self):
         """Measure the sample temperature.
@@ -61,14 +316,16 @@ class AFM (object):
         """
         if hasattr(self.temperature, 'get_temperature'):
             return self.temperature.get_temperature()
+        return self.config['default-temperature']
 
     def move_just_onto_surface(self, depth=-50e-9, setpoint=2,
-                               min_slope_ratio=10, far=200):
+                               min_slope_ratio=10, far=200, steps=20,
+                               sleep=0.0001):
         """Position the AFM tip close to the surface.
 
         Uses `.piezo.get_surface_position()` to pinpoint the position
         of the surface.  Adjusts the stepper position as required via
-        `.stepper.step_relative()` to get within
+        `.stepper.single_step()` to get within
         `2*.stepper.step_size` meters of the surface.  Then adjusts
         the piezo to place the cantilever `depth` meters onto the
         surface.  Negative `depth`\s place the tip off the surface
@@ -83,14 +340,15 @@ class AFM (object):
         stepper_tolerance = 2*self.stepper.step_size
 
         axis = self.piezo.axis_by_name(self.axis_name)
-        defc = self.piezo._deflection_channel()
+        def_config = self.piezo.config.select_config('inputs', 'deflection')
 
         zero = _convert_volts_to_bits(axis.config['channel'], 0)
-        target_def = _convert_volts_to_bits(defc.config, setpoint)
+        target_def = _convert_volts_to_bits(def_config, setpoint)
         self._check_target_deflection(deflection=target_def)
 
         _LOG.debug('zero the %s piezo output' % self.axis_name)
-        self.piezo.jump(axis_name=self.axis_name, position=zero)
+        self.piezo.jump(
+            axis_name=self.axis_name, position=zero, steps=steps, sleep=sleep)
 
         _LOG.debug("see if we're starting near the surface")
         try:
@@ -115,7 +373,7 @@ class AFM (object):
         _LOG.debug('fine tune the stepper position')
         while pos_m < -stepper_tolerance:  # step back if we need to
             _LOG.debug('step back')
-            self.stepper.step_relative(-1, backlash_safe=True)
+            self.stepper.single_step(-1)
             try:
                 pos = self.piezo.get_surface_position(
                     axis_name=self.axis_name, max_deflection=target_def,
@@ -128,7 +386,7 @@ class AFM (object):
                       % (self.stepper.position, pos, pos_m))
         while pos_m > stepper_tolerance:  # step forward if we need to
             _LOG.debug('step forward')
-            self.stepper.step_relative(1)
+            self.stepper.single_step(1)
             try:
                 pos = self.piezo.get_surface_position(
                     axis_name=self.axis_name, max_deflection=target_def,
@@ -144,7 +402,7 @@ class AFM (object):
                   % self.axis_name)
         target_m = pos_m + depth
         target = _convert_meters_to_bits(axis.config, target_m)
-        self.piezo.jump(self.axis_name, target)
+        self.piezo.jump(self.axis_name, target, steps=steps, sleep=sleep)
 
         _LOG.debug(
             'positioned %g m into the surface at stepper %d, piezo %d (%g m)'
@@ -153,16 +411,16 @@ class AFM (object):
     def _check_target_deflection(self, deflection):
         defc = self.piezo._deflection_channel()
         max_def = defc.get_maxdata()
-        if target_deflection > max_def:
-            _LOG.error(('requested setpoint ({:g} V = {:d} bits) is larger '
-                        'than the maximum deflection value of {:d} bits'
-                        ).format(setpoint, target_deflection, max_def))
-            raise ValueError(setpoint)
-        elif target_deflection < 0:
-            _LOG.error(('requested setpoint ({:g} V = {:d} bits) is less '
-                        'than the minimum deflection value of 0 bits'
-                        ).format(setpoint, target_deflection))
-            raise ValueError(setpoint)
+        if deflection > max_def:
+            _LOG.error(('requested setpoint ({} bits) is larger than the '
+                        'maximum deflection value of {} bits'
+                        ).format(deflection, max_def))
+            raise ValueError(deflection)
+        elif deflection < 0:
+            _LOG.error(('requested setpoint ({} bits) is less than the '
+                        'minimum deflection value of 0 bits'
+                        ).format(deflection))
+            raise ValueError(deflection)
 
     def _stepper_approach_again(self, target_deflection, min_slope_ratio, far):
         _LOG.info('back off %d half steps and approach until deflection > %g'
@@ -196,3 +454,12 @@ class AFM (object):
                     cd, target_deflection))
             self.stepper.single_step(1)  # step in
             cd = self.piezo.read_deflection()
+
+    def move_away_from_surface(stepper, distance=None):
+        """Step back approximately `distance` meters.
+        """
+        if distance is None:
+            distance = self.config['far']
+        steps = int(distance/self.stepper.step_size)
+        _LOG.info('step back {} steps (~{} m)' % (steps, distance))
+        self.stepper.step_relative(-steps)