Source code for qcodes_contrib_drivers.drivers.Attocube.ANC300

# -*- coding: utf-8 -*-
"""QCoDeS- Driver for the Attocube ANC300 controller.

This driver can be used with a simulation class (ANC300sim.py) to generate
reasonable answers to all requests. The only thing is to change two times
the comments as shown below (real mode/simulation mode).

Attention - the device has not feedback from the motor. That means, the current position is
not known. The driver can send a move command and the controller behaves like there is a motor
connected to it, even if there is no motor available.

Author:
    Michael Wagener, FZJ / ZEA-2, m.wagener@fz-juelich.de
"""

import time
import logging
import pyvisa

# real mode:
from qcodes.instrument import VisaInstrument
# simulation mode:
#from qcodes_contrib_drivers.drivers.Attocube.ANC300sim import MockVisa

## do not forget to change the main class accordingly:
## real  -> class ANC300(VisaInstrument):
## simul -> class ANC300(MockVisa):

from qcodes.instrument import InstrumentChannel, ChannelList
from qcodes import validators as vals


log = logging.getLogger(__name__)



[docs] class Anc300Axis(InstrumentChannel):
[docs] def __init__(self, parent: 'ANC300', name: str, axis: int, sn: str) -> None: """Creates a new Anc300Axis class instance. The Attocube ANC300 piezo controller has up to 7 axis. Each of them are controlled by the same class. Args: parent: the internal QCoDeS name of the instrument this axis belongs to name: the internal QCoDeS name of the axis itself axis: the Index of the axis (1..7) sn: serial number of the axis controller to change some features Attributes: frequency: Set the frequency of the output signal. The maximum is restricted by the combination of output voltage and Capacitance of the piezo actuators. amplitude: Set the maximum level of the output signal. voltage: (Readonly) Reads the current stepping voltage. offset: Add a constant voltage to the output signal. Attention: the output level is only from 0 to 150 V. filter: Set the filter frequency of the internal low pass filter. For the ANM150 this attribute is not present. For the ANM200 and ANM300 this attribute has different allowed values. mode: Setting to a certain mode typically switches other functionalities off. 'gnd': Setting to this mode diables all outputs and connects them to chassis mass. 'inp': (Not for ANM150) In this mode, AC-IN and DC-IN can be enabled using the specific attributes. Setting to inp mode disables stepping and offset modes. 'cap': Setting to cap mode starts a capacitance measurement. The axis returns to gnd mode afterwards. It is not needed to switch to gnd mode before. 'stp': This enables stepping mode. AC-IN and DC-IN functionalities are not modified, while an offset function would be turned off. 'off': This enables offset mode. AC-IN and DC-IN functionalities are not modified, while any stepping would be turned off. 'stp+': This enables additive offset + stepping mode. Stepping waveforms are added to an offset. AC-IN and DC-IN functionalities are not modified. 'stp-': This enables subtractive offset + stepping mode. Stepping waveforms are subtracted from an offset. AC-IN and DC-IN functionalities, are not modified. ac: When switching on the AC-IN feature, a voltage of up to 10 VAC can be added to the output (gain 1, no amplification) using the AC-IN BNC on the frontplate of the module. dc: When switching on the DC-IN feature, a voltage in the range -10 .. +10 V can be added to the output. The gain is 15. move: Start the movement with the given steps. For moving out use positive numbers and to move in use negative numbers. start: Start a continous movement in the given direction. triggerUp: Set/get input trigger up number on axis triggerDown: Set/get input trigger down number on axis """ super().__init__(parent, name) self._axisnr = axis if sn != 'ANM200': self.add_parameter('frequency', label='Set/get the stepping frequency', get_cmd='getf {}'.format(axis), set_cmd='setf {}'.format(axis)+' {}', vals=vals.Ints(1, 10000), get_parser=int, unit='Hz', docstring=""" Set the frequency of the output signal. The maximum is restricted by the combination of output voltage and Capacitance of the piezo actuators. """ ) self.add_parameter('amplitude', label='Set/get the stepping amplitude', get_cmd='getv {}'.format(axis), set_cmd='setv {}'.format(axis)+' {}', vals=vals.Numbers(0.0, 150.0), get_parser=float, unit='V', docstring="Set the maximum level of the output signal." ) self.add_parameter('voltage', label='Set/get the stepping voltage', get_cmd='geto {}'.format(axis), set_cmd=False, get_parser=float, unit='V', docstring="Reads the current stepping voltage." ) self.add_parameter('offset', label='Set/get the offset voltage', get_cmd='geta {}'.format(axis), set_cmd='seta {}'.format(axis)+' {}', vals=vals.Numbers(0.0, 150.0), get_parser=float, unit='V', docstring=""" Add a constant voltage to the output signal. Attention: the output level is only from 0 to 150 V. """ ) if sn == 'ANM200': self.add_parameter('filter', label='Set/get filter setting', get_cmd='getfil {}'.format(axis), set_cmd='setfil {}'.format(axis)+' {}', vals=vals.Enum('1.6', '16', '160', '1600'), unit='Hz', docstring="Set the filter frequency of the internal low pass filter." ) if sn == 'ANM300': self.add_parameter('filter', label='Set/get filter setting', get_cmd='getfil {}'.format(axis), set_cmd='setfil {}'.format(axis)+' {}', vals=vals.Enum('off', '16', '160'), unit='Hz', docstring="Set the filter frequency of the internal low pass filter." ) if sn == 'ANM150': mode_vals = ['gnd', 'cap', 'stp', 'off', 'stp+', 'stp-'] elif sn == 'ANM200': mode_vals = ['gnd', 'cap', 'stp', 'off', 'stp+', 'stp-', 'inp'] else: # ANM300 mode_vals = ['gnd', 'cap', 'stp', 'off', 'stp+', 'stp-', 'inp'] mode_docs = """ 'gnd': Setting to this mode diables all outputs and connects them to chassis mass. 'cap': Setting to cap mode starts a capacitance measurement. The axis returns to gnd mode afterwards. It is not needed to switch to gnd mode before. 'stp': This enables stepping mode. AC-IN and DC-IN functionalities are not modified, while an offset function would be turned off. 'off': This enables offset mode. AC-IN and DC-IN functionalities are not modified, while any stepping would be turned off. 'stp+': This enables additive offset + stepping mode. Stepping waveforms are added to an offset. AC-IN and DC-IN functionalities are not modified. 'stp-': This enables subtractive offset + stepping mode. Stepping waveforms are subtracted from an offset. AC-IN and DC-IN functionalities, are not modified. """ if 'inp' in mode_vals: mode_docs += """ 'inp': In this mode, AC-IN and DC-IN can be enabled using the specific attributes. Setting to inp mode disables stepping and offset modes. """ self.add_parameter('mode', label='Set/get mode', get_cmd='getm {}'.format(axis), set_cmd='setm {}'.format(axis)+' {}', vals=vals.Enum(*mode_vals), docstring=""" Setting to a certain mode typically switches other functionalities off. Especially, there are the following modes: """ + mode_docs ) if sn != 'ANM150': self.add_parameter('ac', label='Set/get status of AC-IN input', get_cmd='getaci {}'.format(axis), set_cmd='setaci {}'.format(axis)+' {}', vals=vals.Enum('off', 'on'), docstring=""" When switching on the AC-IN feature, a voltage of up to 10 VAC can be added to the output (gain 1, no amplification) using the AC-IN BNC on the frontplate of the module. """ ) self.add_parameter('dc', label='Set/get status of DC-IN input', get_cmd='getdci {}'.format(axis), set_cmd='setdci {}'.format(axis)+' {}', vals=vals.Enum('off', 'on'), docstring=""" When switching on the DC-IN feature, a voltage in the range -10 .. +10 V can be added to the output. The gain is 15. """ ) self.add_parameter('move', label='Move steps', get_cmd=False, set_cmd=self._domove, vals=vals.Ints(), docstring=""" Start the movement with the given steps. For moving out use positive numbers and to move in use negative numbers. """ ) self.add_parameter('start', label='Move continously', get_cmd=False, set_cmd=self._contmove, vals=vals.Enum('up', 'down'), docstring="Start a continous movement in the given direction." ) self.add_parameter('triggerUp', label='Set/get input trigger up number on axis', get_cmd='gettu {}'.format(axis), set_cmd='settu {}'.format(axis)+' {}', vals=vals.Enum('off', '1', '2', '3', '4', '5', '6', '7'), docstring="Set/get input trigger up number on axis" ) self.add_parameter('triggerDown', label='Set/get input trigger down numbers on axis', get_cmd='gettd {}'.format(axis), set_cmd='settd {}'.format(axis)+' {}', vals=vals.Enum('off', '1', '2', '3', '4', '5', '6', '7'), docstring="Set/get input trigger down number on axis" )
def _domove(self, value: int): """ Internal helper function to start the movement. This will not wait until the move is finished. So multiple axis can be started one after the other. Args: value: the amount of steps to move, the sign denotes the direction Returns: None Raises: ValueError: if the value is zero """ if value < 0: self._parent.write('stepd {} {}'.format(self._axisnr, -value)) elif value > 0: self._parent.write('stepu {} {}'.format(self._axisnr, value)) else: raise ValueError("zero is an invalid move parameter") def _contmove(self, direc: str): """ Internal helper function to start the continous movement. This will not wait until the move is finished. So multiple axis can be started one after the other. Args: direc: the direction 'up' or 'down' Returns: None Raises: ValueError: if the given direction is invalid """ if direc == 'up': self._parent.write('stepu {} c'.format(self._axisnr)) elif direc == 'down': self._parent.write('stepd {} c'.format(self._axisnr)) else: raise ValueError("no 'up' or 'donw' given")
[docs] def waitMove(self, wait=1.0, timeout=0): """Global function to wait until the movement is finished. The commandinterface has the function 'stepw n' to wait until the axis stops moving. The controller sends the 'OK' after the axis stops, so the communication is hanging. In the former version with the pyserial interface, it will work fine. But the visa library throws an error if the communication timed out. After this, the read function didn't get the needed 'OK'. To avoid this, this routine asks the current output voltage. This voltage will be zero if the axis has stopped. Args: wait: time to wait between the checks timeout: number of seconds to generate a RuntimeError if not finished moving Returns: None. This function will block, until the motion of this axis has been stopped. """ start = time.time() while True: volt = self._parent.ask('geto {}'.format(self._axisnr)) if float(volt) == 0.0: return time.sleep(wait) if timeout > 0: if time.time() - start >= timeout: raise RuntimeError('waitMove timed out')
[docs] def stopMove(self): """ Global function to stop the movement. """ self._parent.write('stop {}'.format(self._axisnr))
[docs] class Anc300TriggerOut(InstrumentChannel):
[docs] def __init__(self, parent: 'ANC300', name: str, num: int) -> None: """The Attocube ANC300 piezo controller has three trigger outputs. This function cannot be tested because this function belongs to a specific controller feature code. This code was not available during the tests. Args: parent: the internal QCoDeS name of the instrument this output belongs to name: the internal QCoDeS name of the output itself num: the Index of the trigger output Attributes: state: Set / get the state of the output """ super().__init__(parent, name) self.add_parameter('state', label='Set/get trigger output level', get_cmd='getto {}'.format(num), set_cmd='setto {}'.format(num)+' {}', val_mapping={'off': 0, 'on': 1}, vals=vals.Enum('off', 'on'), docstring="Sets the trigger output signal" )
[docs] class ANC300(VisaInstrument): #class ANC300(MockVisa): """ This is the qcodes driver for the Attocube ANC300. Be careful to correct the parameters if not useing the USB port. Status: coding: finished communication tests: done usage in experiment: not yet """ def __init__(self, name, address, **kwargs): super().__init__(name, address, 5, '\r\n', **kwargs) # configure the port if 'ASRL' not in address: self.visa_handle.baud_rate = 38400 # USB has no baud rate parameter self.visa_handle.stop_bits = pyvisa.constants.StopBits.one self.visa_handle.parity = pyvisa.constants.Parity.none self.visa_handle.read_termination = '\r\n' self.parameters.pop('IDN') # Get rid of this parameter # for security check the ID from the device self.idn = self.ask("ver") log.debug("Main version:", self.idn) if not self.idn.startswith("attocube ANC300"): raise RuntimeError("Invalid device ID found: "+str(self.idn)) # Now request all serial numbers of the axis modules. The first 6 chars are the id # of the module type. This will be used to enable special parameters. # Instantiate the axis channels only for available axis axischannels = ChannelList(self, "Anc300Channels", Anc300Axis, snapshotable=False) for ax in range(1, 7+1): try: tmp = self.ask('getser {}'.format(ax)) name = 'axis{}'.format(ax) axischan = Anc300Axis(self, name, ax, tmp[:6]) axischannels.append(axischan) self.add_submodule(name, axischan) except: pass axischannels.lock() self.add_submodule('axis_channels', axischannels) # instantiate the trigger channels even if they could not be tested triggerchannels = ChannelList(self, "Anc300Trigger", Anc300TriggerOut, snapshotable=False) for ax in [1, 2, 3]: name = 'trigger{}'.format(ax) trigchan = Anc300TriggerOut(self, name, ax) triggerchannels.append(trigchan) self.add_submodule(name, trigchan) triggerchannels.lock() self.add_submodule('trigger_channels', triggerchannels)
[docs] def write_raw(self, cmd: str) -> None: """Write cmd and wait until the 'OK' or 'ERROR' comes back from the device. Args: cmd: Command to write to controller. Returns: None Raises: RuntimeError: if Error-Message from the device is read. """ status = super().ask_raw(cmd) # send the command to the device and read the echo/status if status == cmd: # now the device sends an echo status = self.visa_handle.read() # read the status line again if status.startswith('OK'): return if status.startswith('ERROR'): raise RuntimeError(status) # the line before the 'ERROR' a message will be send from the device response = status status = self.visa_handle.read() # read the last status line if status.startswith('ERROR'): raise RuntimeError(response) return
[docs] def ask_raw(self, cmd: str) -> str: """Query instrument with cmd and return response. Args: cmd: Command to write to controller. Returns: Response of Attocube controller to the query. Raises: RuntimeError: if Error-Message from the device is read. """ response = super().ask_raw(cmd) # send the command to the device and read the echo/status if response.startswith('> '): # sometimes the response starts with '> '. I don't know why. response = response[2:] if response == cmd: # now the device has send an echo response = self.visa_handle.read() # read the response line again if response.startswith('> '): response = response[2:] status = self.visa_handle.read() # read the status line if status.startswith('OK'): if '=' in response: # "frequency = 220 Hz" -> filter the 220 tmp = response.split('=') return tmp[1].split()[0] # a single value return response # the complete string if status.startswith('ERROR'): raise RuntimeError(response) # the 'ver' command answers with two lines... response = response + " - " + status status = self.visa_handle.read() # read the second status line if status.startswith('ERROR'): raise RuntimeError(response) return response
[docs] def stopall(self): """ Routine to stop all axis, regardless if the axis is available """ self.log.debug("Stop all axis.") for a in range(7): self.write('stop {}'.format(a+1))
[docs] def close(self): """ Override of the base class' close function """ self.log.debug("Close the device.") super().close()
[docs] def version(self): """ Read all possible version informations. Args: None Returns: Dict with all version informations """ retval = dict() retval['Version'] = self.ask('ver') retval['ContrSN'] = self.ask('getcser') for i in range(7): try: retval['SN{}'.format(i+1)] = self.ask('getser {}'.format(i+1)) except: # if the axis module is not installed ... retval['SN{}'.format(i+1)] = 'EMPTY' return retval
[docs] def getall(self, submod="*"): """ Read all parameters and retun them to the caller. This will scan all submodules with all parameters, so in this function no changes are necessary for new modules or parameters. Args: submod: (optional) returns only the parameters for this submodule Returns: dict with all parameters, the key is the modulename and the parametername """ retval = dict() if submod == "*": # ID and options only if all modules are returned retval.update(self.version()) for m in self.submodules: mod = self.submodules[m] if not isinstance(mod, ChannelList) and (submod in ("*", m)): for p in mod.parameters: par = mod.parameters[p] try: if par.unit: val = str(par()).strip() + " " + par.unit else: val = str(par()).strip() except: val = "** not readable **" retval.update({m + "." + p: val}) return retval