Source code for pylablib.devices.SmarAct.MCS2

from .SmarActControl_lib import wlib as lib
from .SmarActControl_lib import nameprops, keyprops, SmarActControlLibError
from .SmarActControl_defs import SA_CTL_ERROR, SA_CTL_MOVE_MODE, SA_CTL_UNIT
from .base import SmarActError
from ...core.utils import general, py3, funcargparse
from ...core.devio import interface
from ..interface import stage

from ..utils import load_lib

import collections
import contextlib
import time


[docs] class LibraryController(load_lib.LibraryController): pass
libctl=LibraryController(lib)
[docs] def list_devices(): """List all connected SmarAct MCS2 devices""" with libctl.temp_open(): devs=py3.as_str(lib.SA_CTL_FindDevices("")) return [d.strip() for d in devs.split("\n") if d.strip()]
[docs] def get_devices_number(): """Get number of connected SmarAct MCS2 controller""" return len(list_devices())
[docs] def get_SDK_version(): """Get version of MCS2 SDK""" with libctl.temp_open(): return py3.as_str(lib.SA_CTL_GetFullVersionString())
TDeviceInfo=collections.namedtuple("TDeviceInfo",["serial","name"]) TCLMoveParams=collections.namedtuple("TCLMoveParams",["velocity","acceleration","max_step_frequency","hold_time"]) TStepMoveParams=collections.namedtuple("TStepMoveParams",["frequency","amplitude"]) TScanMoveParams=collections.namedtuple("TScanMoveParams",["velocity"])
[docs] class MCS2(stage.IMultiaxisStage): """ SmarAct MCS2 translation stage controller. Args: locator(str): controller locator (returned by :func:`get_devices_number` function) """ Error=SmarActError def __init__(self, locator): super().__init__(default_axis=0) self.locator=locator self.handle=None self._opid=None self.open() self._setup_axes() self._add_info_variable("device_info",self.get_device_info) self._add_status_variable("properties",self.get_all_properties,priority=-2) self._add_status_variable("device_status",self.get_device_status) self._add_status_variable("module_status",lambda: self.get_module_status("all")) self._add_status_variable("axis_status",lambda: self.get_status("all")) self._add_status_variable("position",lambda: self.get_position("all")) self._add_status_variable("target_position",lambda: self.get_target_position("all")) self._add_status_variable("scan_position",lambda: self.get_scan_position("all")) def axis_caller(func, tup=True): def wrapped(params): return [(func(*p,axis=i) if tup else func(p,axis=i)) for i,p in enumerate(params) if i<self.naxes] return wrapped self._add_settings_variable("cl_move_parameters",lambda: self.get_cl_move_parameters(axis="all"), axis_caller(self.setup_cl_move)) self._add_settings_variable("step_move_parameters",lambda: self.get_step_move_parameters(axis="all"), axis_caller(self.setup_step_move)) self._add_settings_variable("scan_move_parameters",lambda: self.get_scan_move_parameters(axis="all"), axis_caller(self.setup_scan_move)) self._add_settings_variable("range_limit",lambda: self.get_range_limit(axis="all"), axis_caller(self.set_range_limit,tup=False)) def _get_connection_parameters(self): return self.locator
[docs] def open(self): """Open the connection to the stage""" if self._opid is None: with libctl.temp_open(): self.handle=lib.SA_CTL_Open(self.locator,"") self._opid=libctl.open().opid
[docs] def close(self): """Close the connection to the stage""" if self._opid is not None: lib.SA_CTL_Close(self.handle) self.handle=None libctl.close(self._opid) self._opid=None
def _setup_axes(self): with self._close_on_error(): self.naxes=self.get_property("number_of_channels") self._update_axes(list(range(self.naxes))) self.nmodules=self.get_property("number_of_bus_modules") self._base_units={ax:self.get_property("pos_base_unit",ax) for ax in range(self.naxes)}
[docs] def is_opened(self): return self.handle is not None
def _get_property_by_key(self, key, idx, kind="i32"): if kind=="i32": return lib.SA_CTL_GetProperty_i32(self.handle,idx,key) if kind=="i64": return lib.SA_CTL_GetProperty_i64(self.handle,idx,key) if kind=="str": return py3.as_str(lib.SA_CTL_GetProperty_s(self.handle,idx,key)) raise ValueError("unrecognized property kind: {}".format(kind))
[docs] def get_property(self, name, idx=0): """Get stage property with the given name and index""" key,_,kind=nameprops[name] return self._get_property_by_key(key,idx,kind)
[docs] def get_all_properties(self, scope="all", idx="all"): """ Get all controller properties within the given scope and for the given index. `scope` can be ``"dev"`` (device properties), ``"mod"`` (module properties), ``"cha"`` (channel properties), or ``"api"`` (api properties); it can also be a list of several scopes, or ``"all"``, which includes all properties. `idx` is the index and usually applies to ``"cha"`` or ``"mod"`` scopes; for other scopes it should be set to 0 or ``"all"``. """ if scope=="all": scope=["dev","mod","cha","api"] elif not isinstance(scope,list): scope=[scope] for s in scope: funcargparse.check_parameter_range(s,"scope",["dev","mod","cha","api"]) props={} nscp={"dev":1,"mod":self.nmodules,"cha":self.naxes,"api":1} for key,(name,scp,kind) in keyprops.items(): if scp not in scope: continue try: if idx=="all": if scp in ["dev","api"]: props[name]=self._get_property_by_key(key,0,kind) else: for i in range(nscp[scp]): props.setdefault(name,{})[i]=self._get_property_by_key(key,i,kind) elif idx<nscp[scp]: props[name]=self._get_property_by_key(key,idx,kind) except SmarActControlLibError as err: if err.code not in [SA_CTL_ERROR.SA_CTL_ERROR_FEATURE_NOT_IMPLEMENTED, SA_CTL_ERROR.SA_CTL_ERROR_FEATURE_NOT_SUPPORTED, SA_CTL_ERROR.SA_CTL_ERROR_PERMISSION_DENIED]: raise return props
def _set_property_by_key(self, key, value, idx, kind="i32"): if kind=="i32": return lib.SA_CTL_SetProperty_i32(self.handle,idx,key,int(value)) if kind=="i64": return lib.SA_CTL_SetProperty_i64(self.handle,idx,key,int(value)) if kind=="str": return lib.SA_CTL_SetProperty_s(self.handle,idx,key,str(value)) raise ValueError("unrecognized property kind: {}".format(kind))
[docs] def set_property(self, name, value, idx=0): """Set stage property with the given name and index""" key,_,kind=nameprops[name] return self._set_property_by_key(key,value,idx,kind)
[docs] def get_device_info(self): """ Get the device info of the controller board. Return tuple ``(serial, name)``. """ return TDeviceInfo(self.get_property("device_serial_number"),self.get_property("device_name"))
[docs] def get_default_axis(self): """Get the default axis (the one automatically applied to channel-related methods)""" return self._default_axis
[docs] def set_default_axis(self, axis): """ Set the default axis (the one automatically applied to channel-related methods). Can be a zero-based axis index or ``"all"`` """ funcargparse.check_parameter_range(axis,"axis",list(range(self.naxes))+["all"]) self._default_axis=axis
[docs] @contextlib.contextmanager def using_default_axis(self, axis): """Context manager for temporarily changing the default axis""" dax=self.get_default_axis() self.set_default_axis(axis) try: yield finally: self.set_default_axis(dax)
_axis_status_bits={ "moving":0x0001,"closed_loop":0x0002,"calibrating":0x004,"referencing":0x0008, "move_delayed":0x0010,"sensor_present":0x0020,"calibrated":0x0040,"referenced":0x0080, "end_stop_reached":0x0100,"range_limit_reached":0x0200,"following_limit_reached":0x0400, "move_failed":0x0800,"streaming":0x1000,"over_temperature":0x400,"reference_mark":0x8000} _moving_status_bits=0x0001|0x0004|0x0008
[docs] @stage.muxaxis def get_status_n(self, axis=None): """Get axis status as an integer""" return self.get_property("channel_state",axis)
[docs] @stage.muxaxis def get_status(self, axis=None): """Get axis status as a set of string descriptors""" statn=self.get_property("channel_state",axis) return [k for k,m in self._axis_status_bits.items() if statn&m]
[docs] @stage.muxaxis def is_moving(self, axis=None): """Check if a given axis is moving (including referencing and calibrating)""" return bool(self.get_property("channel_state",axis)&self._moving_status_bits)
[docs] def wait_move(self, axis, timeout=30.): """Wait for a given axis to stop moving""" if axis=="all": for ax in self.get_all_axes(): self.wait_move(ax,timeout=timeout) return countdown=general.Countdown(timeout) while True: if not self.get_property("channel_state",axis)&self._moving_status_bits: return if countdown.passed(): raise SmarActError("waiting timed out") time.sleep(1E-2)
_dev_status_bits={ "hm_present":0x0001,"movement_locked":0x0002,"internal_comm_failure":0x0100,"streaming":0x1000}
[docs] def get_device_status_n(self): """Get device status as an integer""" return self.get_property("device_state")
[docs] def get_device_status(self): """Get axis status as a set of string descriptors""" statn=self.get_property("device_state") return [k for k,m in self._dev_status_bits.items() if statn&m]
_mod_status_bits={ "sm_present":0x0001,"booster_present":0x0002,"adj_active":0x0004,"iom_present":0x0008, "internal_comm_failure":0x0100,"high_voltage_failure":0x1000,"high_voltage_overload":0x2000,"over_temperature":0x4000}
[docs] def get_module_status_n(self, index=0): """Get module status as an integer""" if index=="all": return [self.get_module_status_n(i) for i in range(self.nmodules)] return self.get_property("module_state",index)
[docs] def get_module_status(self, index): """Get module status as a set of string descriptors""" if index=="all": return [self.get_module_status(i) for i in range(self.nmodules)] statn=self.get_property("module_state",index) return [k for k,m in self._mod_status_bits.items() if statn&m]
def _p2d(self, value, axis, avoid_zero=False): u=self._base_units[axis] if u==SA_CTL_UNIT.SA_CTL_UNIT_METER: dvalue=int(value/1E-12) elif u==SA_CTL_UNIT.SA_CTL_UNIT_DEGREE: dvalue=int(value/1E-9) else: raise ValueError("unrecognized axis units: {}".format(u)) if avoid_zero and value!=0: dvalue=max(dvalue,1) return dvalue def _d2p(self, value, axis): u=self._base_units[axis] if u==SA_CTL_UNIT.SA_CTL_UNIT_METER: return value*1E-12 if u==SA_CTL_UNIT.SA_CTL_UNIT_DEGREE: return value*1E-9 raise ValueError("unrecognized axis units: {}".format(u))
[docs] @stage.muxaxis def get_cl_move_parameters(self, axis=None): """ Get closed-loop move parameters. Return tuple ``(velocity, acceleration, max_step_frequency, hold_time)`` with the maximal move velocity (in m/s or deg/s), move acceleration (in m/s^2 or deg/s^2), maximal step frequency (in Hz), and position hold time (in s, or ``"inf"`` if it is infinite) """ velocity=self._d2p(self.get_property("move_velocity",axis),axis) acceleration=self._d2p(self.get_property("move_acceleration",axis),axis) max_step_frequency=self.get_property("max_cl_frequency",axis) hold_time=self.get_property("hold_time",axis) hold_time=(hold_time%2**32)/1E3 if hold_time>=0 else "inf" return TCLMoveParams(velocity,acceleration,max_step_frequency,hold_time)
[docs] @stage.muxaxis(mux_argnames=["velocity","acceleration","max_step_frequency","hold_time"]) def setup_cl_move(self, velocity=None, acceleration=None, max_step_frequency=None, hold_time=None, axis=None): """ Set closed-loop move parameters. For the meaning of the parameters, see :meth:`get_cl_move_parameters`. Note that changing the hold time will only apply after the next move command. To apply it without actual moving, you can call :meth:`move_by` method with ``distance=0`` for the appropriate axis. If any parameter is ``None``, use the current value. """ if velocity is not None: self.set_property("move_velocity",self._p2d(velocity,axis,avoid_zero=True),axis) if acceleration is not None: self.set_property("move_acceleration",self._p2d(acceleration,axis,avoid_zero=True),axis) if max_step_frequency is not None: self.set_property("max_cl_frequency",max_step_frequency,axis) if hold_time is not None: hold_time=-1 if hold_time=="inf" or hold_time<0 else min(int(hold_time*1E3),2**31) self.set_property("hold_time",hold_time,axis) return self.get_cl_move_parameters(axis)
[docs] @stage.muxaxis def get_step_move_parameters(self, axis=None): """ Get step move parameters. Return tuple ``(frequency, amplitude)`` with the step frequency (in Hz) and step amplitude (normalized between 0 and 1). """ frequency=self.get_property("step_frequency",axis) amplitude=self.get_property("step_amplitude",axis)/65535 return TStepMoveParams(frequency,amplitude)
[docs] @stage.muxaxis(mux_argnames=["frequency","amplitude"]) def setup_step_move(self, frequency=None, amplitude=None, axis=None): """ Set step move parameters. For the meaning of the parameters, see :meth:`get_step_move_parameters`. If any parameter is ``None``, use the current value. """ if frequency is not None: self.set_property("step_frequency",int(frequency),axis) if amplitude is not None: self.set_property("step_amplitude",max(1,min(int(amplitude*65535),65535)),axis) return self.get_step_move_parameters(axis)
[docs] @stage.muxaxis def get_scan_move_parameters(self, axis=None): """ Get scan move parameters. Return tuple ``(velocity)`` with the move velocity (amplitude per second; amplitude is normalized between 0 and 1). """ velocity=self.get_property("scan_velocity",axis)/65535 return TScanMoveParams(velocity)
[docs] @stage.muxaxis(mux_argnames=["velocity"]) def setup_scan_move(self, velocity=None, axis=None): """ Set scan move parameters. For the meaning of the parameters, see :meth:`get_scan_move_parameters`. If any parameter is ``None``, use the current value. """ if velocity is not None: self.set_property("step_amplitude",max(1,min(int(velocity*65535),65535)),axis) return self.get_scan_move_parameters(axis)
[docs] @stage.muxaxis def get_range_limit(self, axis=None): """ Get the movement range limit (in m or deg) for the given axis. Return ``(min, max)`` if the limit is active or ``None`` otherwise. """ minlim=self._d2p(self.get_property("range_limit_min",axis),axis) maxlim=self._d2p(self.get_property("range_limit_max",axis),axis) return (minlim,maxlim) if minlim<maxlim else None
[docs] @stage.muxaxis(mux_argnames=["limit"]) def set_range_limit(self, limit, axis=None): """ Set the movement range limit (in m or deg) for the given axis. `limit` is either a tuple ``(min, max)`` if the limit is active, or ``None`` otherwise. """ if limit is None: # minlim=self.get_property("range_limit_min",axis) # self.set_property("range_limit_max",minlim,axis) self.set_property("range_limit_min",0,axis) self.set_property("range_limit_max",0,axis) else: minlim,maxlim=min(limit),max(limit) self.set_property("range_limit_min",self._p2d(minlim,axis),axis) self.set_property("range_limit_max",self._p2d(maxlim,axis),axis) return self.get_range_limit(axis)
[docs] @stage.muxaxis def get_position(self, axis=None): """Get current position (in m or deg) at the given axis""" try: return self._d2p(self.get_property("position",axis),axis) except SmarActControlLibError as err: if err.code not in [SA_CTL_ERROR.SA_CTL_ERROR_NO_SENSOR_PRESENT,SA_CTL_ERROR.SA_CTL_ERROR_SENSOR_DISABLED]: raise
[docs] @stage.muxaxis(mux_argnames=["position"]) def set_position_reference(self, position=0, axis=None): """ Get the current position (in m or deg) at the given axis. This method simply shifts the position sensor reference; the stage does not move. """ try: self.set_property("position",self._p2d(position,axis),axis) return self.get_position(axis) except SmarActControlLibError as err: if err.code not in [SA_CTL_ERROR.SA_CTL_ERROR_NO_SENSOR_PRESENT,SA_CTL_ERROR.SA_CTL_ERROR_SENSOR_DISABLED]: raise
[docs] @stage.muxaxis def get_scan_position(self, axis=None): """Get current scan position (piezo voltage; normalized between 0 and 1) at the given axis""" return self.get_property("scan_position",axis)/65535
[docs] @stage.muxaxis def get_target_position(self, axis=None): """Get current target position (in m or deg) at the given axis""" try: return self._d2p(self.get_property("target_position",axis),axis) except SmarActControlLibError as err: if err.code not in [SA_CTL_ERROR.SA_CTL_ERROR_NO_SENSOR_PRESENT,SA_CTL_ERROR.SA_CTL_ERROR_SENSOR_DISABLED]: raise
[docs] @stage.muxaxis(mux_argnames=["position"]) def move_to(self, position, axis=None): """Move to the given position (in m or deg) at the given axis""" self.set_property("move_mode",SA_CTL_MOVE_MODE.SA_CTL_MOVE_MODE_CL_ABSOLUTE,axis) lib.SA_CTL_Move(self.handle,axis,self._p2d(position,axis),0)
[docs] @stage.muxaxis(mux_argnames=["distance"]) def move_by(self, distance, axis=None): """Move by the given distance (in m or deg) at the given axis""" self.set_property("move_mode",SA_CTL_MOVE_MODE.SA_CTL_MOVE_MODE_CL_RELATIVE,axis) lib.SA_CTL_Move(self.handle,axis,self._p2d(distance,axis),0)
[docs] @stage.muxaxis(mux_argnames=["steps"]) def move_by_steps(self, steps, axis=None): """Move by the given number of steps at the given axis""" self.set_property("move_mode",SA_CTL_MOVE_MODE.SA_CTL_MOVE_MODE_STEP,axis) lib.SA_CTL_Move(self.handle,axis,int(steps),0)
[docs] @stage.muxaxis(mux_argnames=["position"]) def move_scan_to(self, position, axis=None): """Move to the given open-loop position (piezo voltage; normalized between 0 and 1) using just a piezo deflection at the given axis""" self.set_property("move_mode",SA_CTL_MOVE_MODE.SA_CTL_MOVE_MODE_SCAN_ABSOLUTE,axis) lib.SA_CTL_Move(self.handle,axis,max(0,min(int(position*65535),65535)),0)
[docs] @stage.muxaxis(mux_argnames=["distance"]) def move_scan_by(self, distance, axis=None): """Move by the given open-loop distance (piezo voltage; normalized between -1 and 1) using just a piezo deflection at the given axis""" self.set_property("move_mode",SA_CTL_MOVE_MODE.SA_CTL_MOVE_MODE_SCAN_RELATIVE,axis) lib.SA_CTL_Move(self.handle,axis,max(-65535,min(int(distance*65535),65535)),0)
[docs] @stage.muxaxis def stop(self, axis=None): """Stop motion at the given axis""" lib.SA_CTL_Stop(self.handle,axis,0)
[docs] @stage.muxaxis @interface.use_parameters(start_direction="direction") def home(self, axis=None, sync=True, start_direction="+", reverse_direction=False, abort_on_stop=False, auto_zero=False, continue_on_found=False, stop_on_found=False): """ Home (reference) the given axis. If ``sync==True``, wait until the homing is done. The other parameters are flags setting up the referencing behavior. See MCS2 programming manual section on reference marks for the details. """ ropt=0 for v,m in [(not start_direction,0x01),(reverse_direction,0x02),(abort_on_stop,0x04),(auto_zero,0x08),(continue_on_found,0x10),(stop_on_found,0x20)]: if v: ropt|=m self.set_property("referencing_options",ropt,axis) lib.SA_CTL_Reference(self.handle,axis,0) if sync: self.wait_move(axis,timeout=60.)
[docs] @stage.muxaxis @interface.use_parameters(direction="direction") def calibrate(self, axis=None, sync=True, direction="+", detect_code_inversion=False, advanced_sensor_correction=False, limited_stage_range=False): """ Calibrate the given axis. If ``sync==True``, wait until the calibration is done. The other parameters are flags setting up the calibration behavior. See MCS2 programming manual section on calibrating for the details. """ copt=0 for v,m in [(not direction,0x01),(detect_code_inversion,0x02),(advanced_sensor_correction,0x04),(limited_stage_range,0x100)]: if v: copt|=m self.set_property("calibrating_options",copt,axis) lib.SA_CTL_Calibrate(self.handle,axis,0) if sync: self.wait_move(axis,timeout=60.)
[docs] @stage.muxaxis(mux_argnames=["value"]) def lowlevel_move(self, value, axis=None): """ Execute the low-level movement command with the given integer value. The meaning of the value depends on the devices properties (see MCS2 programming manual for the details). This is a low-level method, whose high-level functionality is covered by other move methods. """ lib.SA_CTL_Move(self.handle,axis,int(value),0)
[docs] @stage.muxaxis def lowlevel_reference(self, axis=None): """ Execute the low-level reference command with the given integer value. Exact procedure depends on the devices properties (see MCS2 programming manual for the details). This is a low-level method, whose high-level functionality is covered by the :meth:`home` method. """ lib.SA_CTL_Reference(self.handle,axis,0)
[docs] @stage.muxaxis def lowlevel_calibrate(self, axis=None): """ Execute the low-level calibration command with the given integer value. Exact procedure depends on the devices properties (see MCS2 programming manual for the details). This is a low-level method, whose high-level functionality is covered by the :meth:`calibrate` method. """ lib.SA_CTL_Calibrate(self.handle,axis,0)