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
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):
Coarse positioning.
temperature | temperature.Controller instance or None
Optional temperature monitoring and control.
+
+ >>> from pycomedi.device import Device
+ >>> from pycomedi import constant as _constant
+ >>> import pypiezo.config as _pypiezo_config
+ >>> import pyafm.config as _config
+
+ >>> device = Device('/dev/comedi0')
+ >>> device.open()
+
+ >>> config = _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'] = _config.StepperConfig()
+ >>> config['stepper']['port'] = _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'] = _config.TemperatureConfig()
+ >>> config['temperature']['name'] = 'test temperature'
+
+ >>> afm = AFM(config=config, devices=[device])
+ >>> 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
+ far: 3e-05
+
+ It's hard to test anything else without pugging into an actual AFM.
+
+ >>> device.close()
"""
- 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.
import h5config.config as _config
import h5config.tools as _h5config_tools
+import pycomedi.constant as _constant
+import pypiezo.config as _pypiezo_config
class PackageConfig (_h5config_tools.PackageConfig):
class TemperatureConfig (_config.Config):
- "Configure `calibcant` temperature operation"
+ "Configure a temperature monitor"
settings = [
+ _config.Setting(
+ name='name',
+ help='Monitor name (so the user will know what is measured).',
+ default=None),
_config.ChoiceSetting(
name='units',
help='Units of raw temperature measurements.',
('Celsius', Celsius),
('Kelvin', Kelvin),
]),
+ _config.IntegerSetting(
+ name='controller',
+ help='MTCA controller ID.',
+ default=1),
+ _config.Setting(
+ name='device',
+ help="Serial port you're using to connect to the controller.",
+ default='/dev/ttyS0'),
+ _config.IntegerSetting(
+ name='baudrate',
+ help="Baud rate for which you've configured your controller.",
+ default=9600),
+ _config.IntegerSetting(
+ name='max-current',
+ help="Maxium current (in amps) output by the controller.",
+ default=0),
+ ]
+
+
+class DigitalPortConfig (_config.Config):
+ "Configure a digital input/output port."
+ settings = [
+ _config.Setting(
+ name='name',
+ help="Port name (so the user will know what it's used for).",
+ default=None),
+ _config.Setting(
+ name='device',
+ help='Comedi device.',
+ default='/dev/comedi0'),
+ _config.IntegerSetting(
+ name='subdevice',
+ help='Comedi subdevice index. -1 for automatic detection.',
+ default=-1),
+ _config.ChoiceSetting(
+ name='subdevice-type',
+ help='Comedi subdevice type for autodetection.',
+ choices=[(x.name, x) for x in _constant.SUBDEVICE_TYPE],
+ default=_constant.SUBDEVICE_TYPE.dio),
+ _config.IntegerListSetting(
+ name='channels',
+ help='Subdevice channels to control by index.',
+ default=[0]),
+ _config.ChoiceSetting(
+ name='direction',
+ help='Port direction.',
+ choices=[(x.name, x) for x in _constant.IO_DIRECTION]),
+ ]
+
+
+class StepperConfig (_config.Config):
+ "Configure a stepper motor."
+ settings = [
+ _config.Setting(
+ name='name',
+ help="Motor name (so the user will know what it's used for).",
+ default=None),
+ _config.BooleanSetting(
+ name='full-step',
+ help='Place the stepper in full-step mode (vs. half-step)',
+ default=True),
_config.BooleanSetting(
- name='default',
- help=('The temperature values are defaults (vs. real '
- 'measurements).'),
+ name='logic',
+ help='Place the stepper in active-high mode (vs. active-low)',
default=True),
+ _config.FloatSetting(
+ name='delay',
+ help=('Time delay between steps in seconds, in case the motor '
+ 'response is slower that the digital output driver.'),
+ default=1e-2),
+ _config.FloatSetting(
+ name='step-size',
+ help= 'Approximate step size in meters.' ,
+ default=170e-9),
+ _config.IntegerSetting(
+ name='backlash',
+ help= 'Generous estimate of the backlash length in half-steps.',
+ default=100),
+ _config.ConfigSetting(
+ name='port',
+ help=('Configure the digital port used to communicate with the '
+ 'stepper.'),
+ config_class=DigitalPortConfig,
+ default=None),
+ ]
+
+
+class AFMConfig (_config.Config):
+ "Configure an Atomic Force Microscope (AFM)."
+ settings = [
+ _config.Setting(
+ name='name',
+ help="AFM name (so the user will know what it's used for).",
+ default=None),
+ _config.Setting(
+ name='main-axis',
+ help=("Name of the piezo axis controlling distance from the "
+ "surface."),
+ default=None),
+ _config.ConfigSetting(
+ name='piezo',
+ help='Configure the underlying piezo (fine adjustment).',
+ config_class=_pypiezo_config.PiezoConfig,
+ default=None),
+ _config.ConfigSetting(
+ name='stepper',
+ help='Configure the underlying stepper motor (coarse adjustment).',
+ config_class=StepperConfig,
+ default=None),
+ _config.ConfigSetting(
+ name='temperature',
+ help='Configure the underlying temperature sensor.',
+ config_class=TemperatureConfig,
+ default=None),
+ _config.FloatSetting(
+ name='far',
+ help=('Approximate distance in meters to move away to get "far" '
+ 'from the surface. For possible stepper adjustments while '
+ 'initially locating the surface.'),
+ default=3e-5),
]
--- /dev/null
+# Copyright
+
+from pycomedi.channel import DigitalChannel as _DigitalChannel
+import pypiezo.base as _base
+
+
+class DigitalPort (object):
+ """A digital input/output port (i.e. cluster of channels).
+
+ >>> from pycomedi.device import Device
+ >>> from pyafm.config import DigitalPortConfig
+
+ >>> device = Device('/dev/comedi0')
+ >>> device.open()
+
+ >>> config = DigitalPortConfig()
+ >>> config['channels'] = [1, 2, 3, 4]
+ >>> config['name'] = 'test port'
+
+ >>> port = DigitalPort(config=config, devices=[device])
+ >>> port.write_bitfield(13)
+ >>> port.write([1, 0, 1, 0])
+ >>> port.write_bitfield(0)
+
+ >>> device.close()
+ """
+ def __init__(self, config, devices=None):
+ self.config = config
+ self.subdevice = None
+ self.load_from_config(devices=devices)
+
+ def load_from_config(self, devices):
+ c = self.config # reduce verbosity
+ if self.subdevice is None:
+ device = _base.load_device(filename=c['device'], devices=devices)
+ if c['subdevice'] < 0:
+ self.subdevice = device.find_subdevice_by_type(
+ c['subdevice-type'])
+ else:
+ self.subdevice = device.subdevice(index=c['subdevice'])
+ self.channels = []
+ self.write_mask = 0
+ for index in c['channels']:
+ channel = self.subdevice.channel(
+ index=index, factory=_DigitalChannel)
+ channel.dio_config(c['direction'])
+ self.write_mask &= 1 << index
+ self.channels.append(channel)
+ self.name = c['name']
+
+ def setup_config(self):
+ self.config['device'] = self.subdevice.device.filename
+ self.config['subdevice'] = self.subdevice.index
+ self.config['channels'] = [c.index for c in self.channels]
+ if self.channels:
+ self.config['direction'] = self.channels[0].dio_get_config()
+
+ def write(self, values):
+ value = 0
+ for channel,val in zip(self.channels, values):
+ value &= val * (1 << channel.index)
+ self.write_bitfield(value)
+
+ def write_bitfield(self, value):
+ self.subdevice.dio_bitfield(bits=value, write_mask=self.write_mask)
--- /dev/null
+# Copyright
+
+from __future__ import absolute_import
+
+import stepper as _stepper
+from .digital_port import DigitalPort as _DigitalPort
+
+
+class Stepper(_stepper.Stepper):
+ """Extend `stepper.Stepper` for easy configuration via `h5config`.
+
+ Uses `DigitalPort` for transmitting the output.
+
+ >>> from pycomedi.device import Device
+ >>> from pycomedi import constant as _constant
+ >>> from pyafm.config import StepperConfig, DigitalPortConfig
+
+ >>> device = Device('/dev/comedi0')
+ >>> device.open()
+
+ >>> config = StepperConfig()
+ >>> config['port'] = DigitalPortConfig()
+ >>> config['port']['channels'] = [1, 2, 3, 4]
+ >>> config['port']['direction'] = _constant.IO_DIRECTION.output
+ >>> config['port']['name'] = 'stepper port'
+ >>> config['name'] = 'test stepper'
+
+ >>> s = Stepper(config=config, devices=[device])
+ >>> s.position
+ 0
+ >>> s.single_step(1)
+ >>> s.position
+ 2
+
+ >>> device.close()
+ """
+ def __init__(self, config, devices=None):
+ self.config = config
+ self.port = None
+ self.load_from_config(devices=devices)
+ c = self.config # reduce verbosity
+ super(Stepper, self).__init__(
+ write=self.port.write_bitfield, full_step=c['full-step'],
+ logic=c['logic'], delay=c['delay'], step_size=c['step-size'],
+ backlash=c['backlash'])
+
+ def load_from_config(self, devices):
+ c = self.config # reduce verbosity
+ if self.port is None:
+ self.port = _DigitalPort(config=c['port'], devices=devices)
+ self._write = self.port.write_bitfield
+ self.full_step = c['full-step']
+ self.logic = c['logic']
+ self.delay = c['delay']
+ self.step_size = c['step-size']
+ self.backlash = c['backlash']
+
+ def setup_config(self):
+ self.port.setup_config()
+ self.config['port'] = self.port.config
+ self.config['full-step'] = self.full_step
+ self.config['logic'] = self.logic
+ self.config['delay'] = self.delay
+ self.config['step-size'] = self.step_size
+ self.config['backlash'] = self.backlash
from pypid.backend.melcor import MelcorBackend as _TemperatureBackend
from . import LOG as _LOG
+from .config import Celsius, Kelvin
-class Temperature (_TemperatureBackend):
- def __init__(self, **kwargs):
+class Temperature (object):
+ """A temperature monitor based on the Melcor controller.
+
+ >>> from pyafm.config import TemperatureConfig
+
+ >>> config = TemperatureConfig()
+ >>> t = Temperature(config=config)
+ >>> t.get_temperature() # doctest: +SKIP
+ 297.37
+ >>> t.cleanup()
+ """
+ def __init__(self, config, backend=None):
_LOG.debug('setup temperature monitor')
- super(Temperature, self).__init__(**kwargs)
- self.set_max_mv(max=1) # amp
+ self.config = config
+ self.backend = backend
+ self.load_from_config()
+
+ def load_from_config(self):
+ c = self.config # reduce verbosity
+ _LOG.critical(type(c))
+ _LOG.critical(c.keys())
+ if self.backend is None:
+ self.backend = _TemperatureBackend(
+ controller=c['controller'],
+ device=c['device'],
+ baudrate=c['baudrate'])
+ self.backend.set_max_mv(max=c['max-current']) # amp
+ self.name = self.config['name']
+
+ def setup_config(self):
+ self.config['controller'] = self.backend._controller
+ self.config['device'] = self.backend._client.port
+ self.config['baudrate'] = self.backend._client.baudrate
+ self.config['max-current'] = self.backend.get_max_mv()
+
+ def cleanup(self):
+ try:
+ self.backend.cleanup()
+ except Exception:
+ pass
+ self.backend = None
def get_temperature(self):
- temp = self.get_pv() + 273.15 # return temperature in kelvin
+ temp = self.backend.get_pv()
+ unit = self.config['units']
+ if unit == Kelvin: # convert K -> K
+ pass
+ elif unit == Celsius: # convert C -> K
+ temp += 273.15
+ else:
+ raise NotImplementedError(unit)
_LOG.info('measured temperature of {:g} K'.format(temp))
return temp