diff --git a/docs/components/index.rst b/docs/components/index.rst index a00d7f5..a061e81 100644 --- a/docs/components/index.rst +++ b/docs/components/index.rst @@ -21,7 +21,7 @@ Relative to TEM V1.9 standard scripting adapter: * Camera * Configuration * Gun - * Gun1 (untested) + * Gun1 * Illumination * InstrumentModeControl * Projection @@ -34,10 +34,10 @@ Relative to TEM V1.2 advanced scripting adapter: * Acquisitions * Autoloader - * EnergyFilter (untested) + * EnergyFilter * Phaseplate * PiezoStage (untested) - * Source (untested) + * Source * TemperatureControl * UserDoorHatch (untested) diff --git a/docs/installation.rst b/docs/installation.rst index 7eeb921..39653c7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -60,13 +60,13 @@ Latest supported Python version on Windows XP is 3.4. Download pytemscript and i .. code-block:: python - pip download -d . pytemscript --python-version 34 --only-binary=:all: --platform win32 + pip download -d . pytemscript comtypes==1.2.1 mrcfile==1.3.0 numpy==1.15.4 pillow==5.3.0 typing --python-version 34 --only-binary=:all: --platform win32 Copy downloaded \*.whl files to the target PC and install them: .. code-block:: python - py -m pip install pytemscript --no-index --find-links . + py -m pip install pytemscript typing --no-index --find-links . Testing ------- diff --git a/docs/remote.rst b/docs/remote.rst index 428108e..34f2eef 100644 --- a/docs/remote.rst +++ b/docs/remote.rst @@ -42,6 +42,8 @@ Then you can connect to the server as shown below: Diagnostic messages are saved to ``socket_client.log`` and ``socket_server.log`` as well as printed to the console. Log files are rotated weekly at midnight. +To shutdown pytemscript-server, press Ctrl+C in the console. + UTAPI client ------------ @@ -53,8 +55,7 @@ you can search for ``utapi_server.exe`` in the Task Manager. The server is liste **46699**. Under the hood UTAPI utilizes gRPC (Google Remote Procedure Calls) framework that uses protocol buffers for communication. -Here we provide a Python client that converts API commands to UTAPI calls. -The client requires extra dependencies to be installed: +Pytemscript converts its API commands to UTAPI calls. The client requires extra dependencies to be installed: .. code-block:: python diff --git a/pyproject.toml.future b/pyproject.toml.future index 0aeb88f..29cc41d 100644 --- a/pyproject.toml.future +++ b/pyproject.toml.future @@ -36,7 +36,7 @@ dependencies = [ ] [project.optional-dependencies] -extra = ["matplotlib", "mypy"] +dev = ["matplotlib", "mypy"] utapi = ["grpcio", "grpcio-tools", "protobuf"] [project.urls] diff --git a/pytemscript/__init__.py b/pytemscript/__init__.py index d224066..545f63d 100644 --- a/pytemscript/__init__.py +++ b/pytemscript/__init__.py @@ -1 +1 @@ -__version__ = '3.0b1' +__version__ = '3.0b2' diff --git a/pytemscript/clients/base_client.py b/pytemscript/clients/base_client.py index 81735a7..59ccbd6 100644 --- a/pytemscript/clients/base_client.py +++ b/pytemscript/clients/base_client.py @@ -20,6 +20,11 @@ def has_lowdose_iface(self) -> bool: def has_ccd_iface(self) -> bool: raise NotImplementedError("Method must be implemented in subclass") + @property + @lru_cache(maxsize=1) + def has_calgetter_iface(self) -> bool: + raise NotImplementedError("Method must be implemented in subclass") + def call(self, method: str, body: RequestBody): """ Main method used by modules. """ raise NotImplementedError("Method must be implemented in subclass") diff --git a/pytemscript/clients/com_client.py b/pytemscript/clients/com_client.py index f61e48c..beba13e 100644 --- a/pytemscript/clients/com_client.py +++ b/pytemscript/clients/com_client.py @@ -24,6 +24,7 @@ def __init__(self, self.tem_adv = None self.tem_lowdose = None self.tecnai_ccd = None + self.calgetter = None if platform.system() == "Windows": logging.getLogger("comtypes").setLevel(logging.INFO) @@ -64,6 +65,8 @@ def _initialize(self, useLD: bool, useTecnaiCCD: bool): if self.tecnai_ccd is None: self.tecnai_ccd = self._createCOMObject(SCRIPTING_TECNAI_CCD2) + self.calgetter = self._createCOMObject(CALGETTER) + if self.tem is None: raise RuntimeError("Failed to create COM object.") @@ -73,6 +76,7 @@ def _close(self): self.tem_adv = None self.tem_lowdose = None self.tecnai_ccd = None + self.calgetter = None com_module.CoUninitialize() @@ -124,6 +128,11 @@ def has_lowdose_iface(self) -> bool: def has_ccd_iface(self) -> bool: return self._scope.tecnai_ccd is not None + @property + @lru_cache(maxsize=1) + def has_calgetter_iface(self) -> bool: + return self._scope.calgetter is not None + def _get(self, attrname): return rgetattr(self._scope, attrname) @@ -163,6 +172,9 @@ def _exec_special(self, attrname, **kwargs): if obj_cls is None or obj_method is None: raise AttributeError("obj_class and obj_method must be specified") + logging.debug("=> EXEC_SP: %s.%s, kwargs=%r",obj_cls.__name__, + obj_method, kwargs) + if attrname is None: # plugin case com_obj = self._scope else: @@ -171,7 +183,7 @@ def _exec_special(self, attrname, **kwargs): method = getattr(obj_instance, obj_method) if method is None: - raise AttributeError("Method %s not implemented for %s" % (obj_method, obj_cls)) + raise AttributeError("Method %s not implemented for %s" % (obj_method, obj_cls.__name__)) result = method(**kwargs) diff --git a/pytemscript/clients/socket_client.py b/pytemscript/clients/socket_client.py index 4a29737..d8c4760 100644 --- a/pytemscript/clients/socket_client.py +++ b/pytemscript/clients/socket_client.py @@ -66,6 +66,14 @@ def has_ccd_iface(self) -> bool: return response + @property + @lru_cache(maxsize=1) + def has_calgetter_iface(self) -> bool: + response = self.__send_request({"method": "has_calgetter_iface"}) + logging.debug("Received response: %s", response) + + return response + def call(self, method: str, body: RequestBody): """ Main method used by modules. """ payload = {"method": method, "body": body} diff --git a/pytemscript/modules/acquisition.py b/pytemscript/modules/acquisition.py index 7ffc4ec..02fc0bf 100644 --- a/pytemscript/modules/acquisition.py +++ b/pytemscript/modules/acquisition.py @@ -109,7 +109,7 @@ def show_cameras_cca(self, tem_cameras: Dict) -> Dict: return tem_cameras - def acquire(self, cameraName: str) -> Image: + def acquire(self, cameraName: str, **kwargs) -> Image: """ Perform actual acquisition. Camera settings should be set beforehand. :param cameraName: Camera name @@ -121,7 +121,7 @@ def acquire(self, cameraName: str) -> Image: t0 = time.time() imgs = acq.AcquireImages() t1 = time.time() - image = convert_image(imgs[0], name=cameraName) + image = convert_image(imgs[0], name=cameraName, **kwargs) t2 = time.time() logging.debug("\tAcquisition took %f s" % (t1 - t0)) logging.debug("\tConverting image took %f s" %(t2 - t1)) @@ -130,7 +130,8 @@ def acquire(self, cameraName: str) -> Image: def acquire_advanced(self, cameraName: str, - recording: bool = False) -> Optional[Image]: + recording: bool = False, + **kwargs) -> Optional[Image]: """ Perform actual acquisition with advanced scripting. """ if recording: self.com_object.CameraContinuousAcquisition.Start() @@ -141,7 +142,7 @@ def acquire_advanced(self, img = self.com_object.CameraSingleAcquisition.Acquire() t1 = time.time() #self.com_object.CameraSingleAcquisition.Wait() - image = convert_image(img, name=cameraName, advanced=True) + image = convert_image(img, name=cameraName, advanced=True, **kwargs) t2 = time.time() logging.debug("\tAcquisition took %f s" % (t1 - t0)) logging.debug("\tConverting image took %f s" % (t2 - t1)) @@ -270,23 +271,37 @@ def set_tem_presets_advanced(self, else: raise NotImplementedError("This camera does not support electron counting") - if eer is not None and hasattr(capabilities, 'SupportsEER'): - if capabilities.SupportsEER: + # EER saving is supported in TEM server 7.6 (Titan 3.6 / Talos 2.6) + if hasattr(capabilities, 'SupportsEER'): + eer_is_supported = capabilities.SupportsEER + if eer and eer_is_supported: settings.EER = eer - if eer and not settings.ElectronCounting: - raise RuntimeError("Electron counting should be enabled when using EER") - if eer and 'group_frames' in kwargs: - raise RuntimeError("No frame grouping allowed when using EER") - else: + elif eer and not eer_is_supported: raise NotImplementedError("This camera does not support EER") + elif not eer and eer_is_supported: + # EER param is persistent throughout camera COM object lifetime, + # if not using EER we need to set it to False + settings.EER = False + + if eer and not settings.ElectronCounting: + raise RuntimeError("Electron counting should be enabled when using EER") + if eer and 'group_frames' in kwargs: + raise RuntimeError("No frame grouping allowed when using EER") + + if capabilities.SupportsDoseFractions: + dfd = settings.DoseFractionsDefinition + dfd.Clear() + + if kwargs.get('save_frames'): + if not capabilities.SupportsDoseFractions: + raise NotImplementedError("This camera does not support dose fractions") - if 'save_frames' in kwargs: total = settings.CalculateNumberOfFrames() now = datetime.now() settings.SubPathPattern = cameraName + "_" + now.strftime("%d%m%Y_%H%M%S") output = settings.PathToImageStorage + settings.SubPathPattern - if eer is None: + if eer in [False, None]: group = kwargs.get('group_frames', 1) if group < 1: raise ValueError("Frame group size must be at least 1") @@ -294,15 +309,14 @@ def set_tem_presets_advanced(self, raise ValueError("Frame group size cannot exceed maximum possible " "number of frames: %d. Change exposure time." % total) - dfd = settings.DoseFractionsDefinition - dfd.Clear() frame_ranges = [(i, min(i + group, total)) for i in range(0, total-1, group)] - for i in frame_ranges: + logging.debug("Using frame ranges: %s", frame_ranges[:-1]) + for i in frame_ranges[:-1]: dfd.AddRange(i[0], i[1]) logging.info("Movie of %d fractions (%d frames, group=%d) " "will be saved to: %s.mrc", - len(frame_ranges), total, group, output) + len(frame_ranges)-1, total, group, output) logging.info("MRC format can only contain images of up to " "16-bits per pixel, to get true CameraCounts " "multiply pixels by PixelToValueCameraCounts " @@ -431,7 +445,7 @@ def __acquire_with_tecnaiccd(self, "pass useTecnaiCCD=True to the Microscope() ?") else: logging.info("Using TecnaiCCD plugin for Gatan camera") - from ..plugins.tecnai_ccd_plugin import TecnaiCCDPlugin + from ..plugins.tecnai_ccd import TecnaiCCDPlugin body = RequestBody(attr=None, obj_cls=TecnaiCCDPlugin, @@ -506,7 +520,8 @@ def acquire_tem_image(self, body = RequestBody(attr="tem.Acquisition", obj_cls=AcquisitionObj, obj_method="acquire", - cameraName=cameraName) + cameraName=cameraName, + **kwargs) image = self.__client.call(method="exec_special", body=body) logging.info("TEM image acquired on %s", cameraName) @@ -537,7 +552,8 @@ def acquire_tem_image(self, obj_cls=AcquisitionObj, obj_method="acquire_advanced", cameraName=cameraName, - recording=kwargs["recording"]) + recording=kwargs["recording"], + **kwargs) self.__client.call(method="exec_special", body=body) logging.info("TEM image acquired on %s", cameraName) return None @@ -546,7 +562,8 @@ def acquire_tem_image(self, validator=Image, obj_cls=AcquisitionObj, obj_method="acquire_advanced", - cameraName=cameraName) + cameraName=cameraName, + **kwargs) image = self.__client.call(method="exec_special", body=body) return image @@ -587,7 +604,8 @@ def acquire_stem_image(self, validator=Image, obj_cls=AcquisitionObj, obj_method="acquire", - cameraName=cameraName) + cameraName=cameraName, + **kwargs) image = self.__client.call(method="exec_special", body=body) logging.info("STEM image acquired on %s", cameraName) diff --git a/pytemscript/modules/autoloader.py b/pytemscript/modules/autoloader.py index 114b9fa..0933af2 100644 --- a/pytemscript/modules/autoloader.py +++ b/pytemscript/modules/autoloader.py @@ -13,7 +13,7 @@ def __init__(self, client): self.__id = "tem.AutoLoader" self.__id_adv = "tem_adv.AutoLoader" self.__err_msg = "Autoloader is not available" - self.__err_msg_adv = "This function is not available in your advanced scripting interface." + self.__err_msg_adv = "Autoloader advanced interface is not available. Requires TEM server 7.8+" @property @lru_cache(maxsize=1) diff --git a/pytemscript/modules/energyfilter.py b/pytemscript/modules/energyfilter.py index 7c664a4..af3c653 100644 --- a/pytemscript/modules/energyfilter.py +++ b/pytemscript/modules/energyfilter.py @@ -10,7 +10,7 @@ class EnergyFilter: def __init__(self, client): self.__client = client self.__id = "tem_adv.EnergyFilter" - self.__err_msg = "EnergyFilter interface is not available" + self.__err_msg = "EnergyFilter advanced interface is not available. Requires TEM server 7.8+" @property @lru_cache(maxsize=1) diff --git a/pytemscript/modules/extras.py b/pytemscript/modules/extras.py index 9ec2446..ea6f640 100644 --- a/pytemscript/modules/extras.py +++ b/pytemscript/modules/extras.py @@ -45,7 +45,7 @@ def __init__(self, x: float, y: float) -> None: self.__min = None self.__max = None - def __repr__(self): + def __repr__(self) -> str: return "Vector(x=%f, y=%f)" % (self.x, self.y) def __str__(self): @@ -150,6 +150,9 @@ def __init__(self, else: self.timestamp = datetime.now().strftime("%Y:%m:%d %H:%M:%S") + def __repr__(self) -> str: + return "Image()" + @lru_cache(maxsize=1) def __create_tiff_tags(self): """Create TIFF tags from metadata. """ @@ -229,6 +232,9 @@ class SpecialObj: def __init__(self, com_object): self.com_object = com_object + def __repr__(self): + return "%s()" % self.__class__.__name__ + class StageObj(SpecialObj): """ Wrapper around stage / piezo stage COM object. """ diff --git a/pytemscript/modules/gun.py b/pytemscript/modules/gun.py index 70d30de..06a8dd4 100644 --- a/pytemscript/modules/gun.py +++ b/pytemscript/modules/gun.py @@ -5,7 +5,41 @@ from ..utils.misc import RequestBody from ..utils.enums import FegState, HighTensionState, FegFlashingType -from .extras import Vector +from .extras import Vector, SpecialObj + + +ERR_MSG_GUN1 = "Gun1 interface is not available. Requires TEM server 7.10+" + + +class GunObj(SpecialObj): + """ Wrapper around Gun COM object specifically for the Gun1 interface. """ + def __init__(self, com_object): + super().__init__(com_object) + import comtypes.gen.TEMScripting as Ts + if hasattr(Ts, "Gun1"): + self.gun1 = self.com_object.QueryInterface(Ts.Gun1) + else: + self.gun1 = None + + def is_available(self) -> bool: + """ Gun1 inherits from the Gun interface of the std scripting. """ + return self.gun1 is not None + + def get_hv_offset(self) -> float: + if not self.is_available(): + raise NotImplementedError(ERR_MSG_GUN1) + return self.gun1.HighVoltageOffset + + def set_hv_offset(self, value: float) -> None: + if not self.is_available(): + raise NotImplementedError(ERR_MSG_GUN1) + self.gun1.HighVoltageOffset = value + + def get_hv_offset_range(self) -> Tuple: + if not self.is_available(): + raise NotImplementedError(ERR_MSG_GUN1) + result = self.gun1.GetHighVoltageOffsetRange() + return result[0], result[1] class Gun: @@ -16,15 +50,16 @@ def __init__(self, client): self.__client = client self.__id = "tem.Gun" self.__id_adv = "tem_adv.Source" - self.__err_msg_gun1 = "Gun1 interface is not available. Requires TEM Server 7.10+" self.__err_msg_cfeg = "Source/C-FEG interface is not available" @property @lru_cache(maxsize=1) def __has_gun1(self) -> bool: - body = RequestBody(attr="tem.Gun1", validator=bool) - - return self.__client.call(method="has", body=body) + body = RequestBody(attr=self.__id, + obj_cls=GunObj, + obj_method="is_available", + validator=bool) + return self.__client.call(method="exec_special", body=body) @property @lru_cache(maxsize=1) @@ -73,18 +108,24 @@ def tilt(self, vector: Vector) -> None: def voltage_offset(self) -> float: """ High voltage offset. (read/write)""" if self.__has_gun1: - body = RequestBody(attr="tem.Gun1.HighVoltageOffset", validator=float) - return self.__client.call(method="get", body=body) + body = RequestBody(attr=self.__id, + obj_cls=GunObj, + obj_method="get_hv_offset", + validator=float) + return self.__client.call(method="exec_special", body=body) else: - raise NotImplementedError(self.__err_msg_gun1) + raise NotImplementedError(ERR_MSG_GUN1) @voltage_offset.setter def voltage_offset(self, offset: float) -> None: if self.__has_gun1: - body = RequestBody(attr="tem.Gun1.HighVoltageOffset", value=offset) - self.__client.call(method="set", body=body) + body = RequestBody(attr=self.__id, + obj_cls=GunObj, + obj_method="set_hv_offset", + value=offset) + self.__client.call(method="exec_special", body=body) else: - raise NotImplementedError(self.__err_msg_gun1) + raise NotImplementedError(ERR_MSG_GUN1) @property def feg_state(self) -> str: @@ -155,18 +196,20 @@ def voltage_max(self) -> float: def voltage_offset_range(self): """ Returns the high voltage offset range. """ if self.__has_gun1: - #TODO: this is a function? - body = RequestBody(attr="tem.Gun1.GetHighVoltageOffsetRange()") - return self.__client.call(method="exec", body=body) + body = RequestBody(attr=self.__id, + obj_cls=GunObj, + obj_method="get_hv_offset_range", + validator=tuple) + return self.__client.call(method="exec_special", body=body) else: - raise NotImplementedError(self.__err_msg_gun1) + raise NotImplementedError(ERR_MSG_GUN1) @property def beam_current(self) -> float: - """ Returns the C-FEG beam current in Amperes. """ + """ Returns the C-FEG beam current in nanoAmperes. """ if self.__has_source: body = RequestBody(attr=self.__id_adv + ".BeamCurrent", validator=float) - return self.__client.call(method="get", body=body) + return self.__client.call(method="get", body=body) * 1e9 else: raise NotImplementedError(self.__err_msg_cfeg) diff --git a/pytemscript/modules/illumination.py b/pytemscript/modules/illumination.py index fdc5362..ea33922 100644 --- a/pytemscript/modules/illumination.py +++ b/pytemscript/modules/illumination.py @@ -263,17 +263,20 @@ def beam_tilt(self) -> Union[Vector, float]: depends on a calibration of the tilt angles. (read/write) """ dfmode = RequestBody(attr=self.__id + ".DFMode", validator=int) - dftilt = RequestBody(attr=self.__id + ".Tilt") + dftiltx = RequestBody(attr=self.__id + ".Tilt.X", validator=float) + dftilty = RequestBody(attr=self.__id + ".Tilt.Y", validator=float) mode = self.__client.call(method="get", body=dfmode) - tilt = self.__client.call(method="get", body=dftilt) + tiltx = self.__client.call(method="get", body=dftiltx) # rad + tilty = self.__client.call(method="get", body=dftilty) # rad if mode == DarkFieldMode.CONICAL: - tilt *= 1e3 - return Vector(tilt.x * math.cos(tilt.y), tilt.x * math.sin(tilt.y)) + tilt = tiltx + rot = tilty + return Vector(tilt * math.cos(rot), tilt * math.sin(rot)) * 1e3 elif mode == DarkFieldMode.CARTESIAN: - return tilt * 1e3 - else: + return Vector(tiltx, tilty) * 1e3 + else: # DF is off return Vector(0.0, 0.0) # Microscope might return nonsense if DFMode is OFF @beam_tilt.setter @@ -284,7 +287,7 @@ def beam_tilt(self, tilt: Union[Vector, float]) -> None: if isinstance(tilt, float): tilt = Vector(tilt, tilt) - tilt *= 1e-3 + tilt *= 1e-3 # mrad to rad if tilt == (0.0, 0.0): body = RequestBody(attr=self.__id + ".Tilt", value=tilt) @@ -299,13 +302,9 @@ def beam_tilt(self, tilt: Union[Vector, float]) -> None: body = RequestBody(attr=self.__id + ".Tilt", value=value) self.__client.call(method="set", body=body) - elif mode == DarkFieldMode.OFF: - body = RequestBody(attr=self.__id + ".DFMode", value=DarkFieldMode.CARTESIAN) - self.__client.call(method="set", body=body) - - body = RequestBody(attr=self.__id + ".Tilt", value=tilt.x) + elif mode == DarkFieldMode.CARTESIAN: + body = RequestBody(attr=self.__id + ".Tilt", value=tilt) self.__client.call(method="set", body=body) else: - body = RequestBody(attr=self.__id + ".Tilt", value=tilt.x) - self.__client.call(method="set", body=body) + raise ValueError("Dark field mode is OFF. You cannot set beam tilt.") diff --git a/pytemscript/modules/optics.py b/pytemscript/modules/optics.py index 69669b4..af25a93 100644 --- a/pytemscript/modules/optics.py +++ b/pytemscript/modules/optics.py @@ -1,7 +1,7 @@ import logging from ..utils.misc import RequestBody -from ..utils.enums import ProjectionNormalization, IlluminationNormalization +from ..utils.enums import ProjectionNormalization, IlluminationNormalization, InstrumentMode from .illumination import Illumination from .projection import Projection @@ -15,6 +15,14 @@ def __init__(self, client, condenser_type): self.illumination = Illumination(client, condenser_type) self.projection = Projection(client) + @property + def instrument_mode(self) -> str: + """ Current instrument mode: TEM or STEM. """ + body = RequestBody(attr="tem.InstrumentModeControl.InstrumentMode", validator=int) + result = self.__client.call(method="get", body=body) + + return InstrumentMode(result).name + @property def screen_current(self) -> float: """ The current measured on the fluorescent screen (units: nanoAmperes). """ diff --git a/pytemscript/modules/projection.py b/pytemscript/modules/projection.py index a2d7b49..d287a60 100644 --- a/pytemscript/modules/projection.py +++ b/pytemscript/modules/projection.py @@ -17,7 +17,6 @@ def __init__(self, client): self.__id = "tem.Projection" self.__err_msg = "Microscope is not in diffraction mode" self.__magnifications = OrderedDict() - self.__find_magnifications() def __find_magnifications(self) -> None: if not self.__magnifications: @@ -26,15 +25,10 @@ def __find_magnifications(self) -> None: body = RequestBody(attr="tem.AutoNormalizeEnabled", value=False) self.__client.call(method="set", body=body) - # make sure we are in TEM mode - body = RequestBody(attr="tem.InstrumentModeControl.InstrumentMode", - value=InstrumentMode.TEM) - self.__client.call(method="set", body=body) self.mode = ProjectionMode.IMAGING - saved_index = self.magnification_index previous_index = None - index = 0 + index = 1 while True: self.magnification_index = index index = self.magnification_index @@ -54,6 +48,7 @@ def __find_magnifications(self) -> None: @property def list_magnifications(self) -> Dict: """ List of available magnifications: mag -> (mag_index, submode). """ + self.__find_magnifications() return self.__magnifications @property @@ -70,6 +65,11 @@ def focus(self, value: float) -> None: body = RequestBody(attr=self.__id + ".Focus", value=value) self.__client.call(method="set", body=body) + def eucentric_focus(self) -> None: + """ Reset focus to eucentric value. """ + body = RequestBody(attr=self.__id + ".Focus", value=0) + self.__client.call(method="set", body=body) + @property def magnification(self) -> int: """ The reference magnification value (screen up setting).""" diff --git a/pytemscript/modules/temperature.py b/pytemscript/modules/temperature.py index 22d0fee..2bc4636 100644 --- a/pytemscript/modules/temperature.py +++ b/pytemscript/modules/temperature.py @@ -12,7 +12,7 @@ def __init__(self, client): self.__id = "tem.TemperatureControl" self.__id_adv = "tem_adv.TemperatureControl" self.__err_msg = "TemperatureControl is not available" - self.__err_msg_adv = "This function is not available in your advanced scripting interface." + self.__err_msg_adv = "TemperatureControl advanced interface is not available. Requires TEM server 7.8+" @property @lru_cache(maxsize=1) diff --git a/pytemscript/plugins/calgetter.py b/pytemscript/plugins/calgetter.py new file mode 100644 index 0000000..5ad6746 --- /dev/null +++ b/pytemscript/plugins/calgetter.py @@ -0,0 +1,167 @@ +from typing import Tuple, Dict, Optional +import time +import numpy as np + +from ..utils.enums import BasicTransformTypes, ModeTypes, LorentzTypes, LensSeriesTypes + + +class CalGetterPlugin: + """ Main class that communicates with Calibrations service. """ + def __init__(self, com_iface): + self.cg_iface = com_iface + + def is_connected(self) -> bool: + """ If calgetter.exe is not already running, this call usually starts it.""" + for _ in range(3): + try: + if self.cg_iface.IsConnected: + return True + except: + pass + + time.sleep(1) + + return False + + def get_magnifications(self, + camera: str, + mode: ModeTypes = ModeTypes.NANOPROBE, + series: LensSeriesTypes = LensSeriesTypes.ZOOM, + lorentz: LorentzTypes = LorentzTypes.OFF, + kv: int = 300) -> Optional[Dict]: + """ Returns a dict of calibrated magnifications. + :param camera: Camera name + :param mode: Microprobe or nanoprobe mode + :param series: Zoom of EFTEM projection mode + :param lorentz: Lorentz lens + :param kv: voltage + """ + result = self.cg_iface.ActualMagnifications(camera, + mode.value, + series.value, + lorentz.value, + kv) + if result is not None and type(result[0]) is tuple: + mag_range = {1: "LM", 2: "M", 3: "SA", 4: "Mh"} + mags_dict = { + int(value): { + "calibrated": calibrated, + "index": int(index), + "mode": mag_range[int(mode)], + "rotation": rotation + } + for value, calibrated, index, mode, rotation in zip(result[0], + result[1], + result[2], + result[3], + result[4]) + } + return mags_dict + else: + # not calibrated + return None + + def get_reference_camera(self) -> str: + """ Returns the reference camera name.""" + return self.cg_iface.GetReferenceCameraName() + + def get_camera_pixel_size(self, camera) -> float: + """ Get camera physical pixel size in um at current settings. + :param camera: Camera name + """ + res = self.cg_iface.GetCameraPixelSize(camera) + return res[0] * 1e6 + + def get_camera_rotation(self, camera) -> float: + """ Returns the rotation of the camera relative to the reference camera. + :param camera: Camera name + """ + return self.cg_iface.CameraRotation(camera) + + def get_image_rotation(self, + camera: str, + mode: ModeTypes, + magindex: int, + mag: float, + series: LensSeriesTypes = LensSeriesTypes.ZOOM, + lorentz: LorentzTypes = LorentzTypes.OFF, + kv: int = 300) -> float: + """ Returns the image rotation angle for a specific magnification. + :param camera: Camera name + :param mode: Microprobe or nanoprobe mode + :param magindex: Magnification index + :param mag: Nominal magnification + :param series: Zoom of EFTEM projection mode + :param lorentz: Lorentz lens + :param kv: voltage + """ + return self.cg_iface.ActualTemRotation(camera, + mode.value, + magindex, + mag, + series.value, + lorentz.value, + kv) + + def get_image_pixel_size(self, + camera: str, + mode: ModeTypes, + magindex: int, + mag: float, + series: LensSeriesTypes = LensSeriesTypes.ZOOM, + lorentz: LorentzTypes = LorentzTypes.OFF, + kv: int = 300) -> float: + """ Returns the image pixel size for a specific magnification in meters. + :param camera: Camera name + :param mode: Microprobe or nanoprobe mode + :param magindex: Magnification index + :param mag: Nominal magnification + :param series: Zoom of EFTEM projection mode + :param lorentz: Lorentz lens + :param kv: voltage + """ + res = self.cg_iface.GetPhysicalPixelSize(camera, + mode.value, + magindex, + mag, + series.value, + lorentz.value, + kv) + return res[0] + + def basic_transform(self, + transform_type: BasicTransformTypes, + input_matrix: np.ndarray = None, + x: float = 0, + y: float = 0, + x_ref: float = 0, + y_ref: float = 0) -> Tuple[float, float]: + """ Transform a vector from one coordinate system to another. + :param transform_type: Transformation type + :param input_matrix: Input matrix + :param x: input x value + :param y: input y value + :param x_ref: input x reference value + :param y_ref: input y reference value + + Input matrix must be 2D: + A = np.array([[a11, a12, a13], + [a21, a22, a23]]) + """ + if input_matrix is None: + input_matrix = np.zeros((2, 3)) + + assert input_matrix.ndim == 2 + + x_out, y_out = self.cg_iface.BasicTransform( + transform_type.value, + input_matrix[0, 0], + input_matrix[0, 1], + input_matrix[0, 2], + input_matrix[1, 0], + input_matrix[1, 1], + input_matrix[1, 2], + x, y, + 0, 0, + x_ref, y_ref) + return x_out, y_out diff --git a/pytemscript/plugins/tecnai_ccd_plugin.py b/pytemscript/plugins/tecnai_ccd.py similarity index 91% rename from pytemscript/plugins/tecnai_ccd_plugin.py rename to pytemscript/plugins/tecnai_ccd.py index 6bd02b1..f8985bc 100644 --- a/pytemscript/plugins/tecnai_ccd_plugin.py +++ b/pytemscript/plugins/tecnai_ccd.py @@ -1,6 +1,5 @@ import logging import time -from typing import Optional from ..utils.enums import AcqImageSize, AcqMode, AcqSpeed from ..modules.extras import Image @@ -8,7 +7,10 @@ class TecnaiCCDPlugin: - """ Main class that uses FEI Tecnai CCD plugin on microscope PC. """ + """ Main class that uses Tecnai CCD plugin on microscope PC + to communicate with Gatan Digital Micrograph. + Starting from TIA 4.10 TecnaiCCD.dll was replaced by FeiCCD.dll + """ def __init__(self, com_iface): self.ccd_plugin = com_iface.tecnai_ccd self._img_params = dict() @@ -37,12 +39,18 @@ def acquire_image(self, #img = self.ccd_plugin.AcquireImageShown() #img = self.ccd_plugin.AcquireDarkSubtractedImage() # variant + t0 = time.time() img = self.ccd_plugin.AcquireRawImage() # variant / tuple + t1 = time.time() if kwargs.get('show', False): self.ccd_plugin.ShowAcquiredImage() image = convert_image(img, name=cameraName, use_safearray=False, **self._img_params) + t2 = time.time() + logging.debug("\tAcquisition took %f s" % (t1 - t0)) + logging.debug("\tConverting image took %f s" % (t2 - t1)) + return image else: raise Exception("Camera is busy acquiring...") diff --git a/pytemscript/utils/constants.py b/pytemscript/utils/constants.py index 3d94e28..e40854e 100644 --- a/pytemscript/utils/constants.py +++ b/pytemscript/utils/constants.py @@ -1,16 +1,15 @@ -SCRIPTING_TECNAI = "Tecnai.Instrument" +SCRIPTING_TECNAI = "Tecnai.Instrument" # used only for Tecnai < 3.1.1 SCRIPTING_STD = "TEMScripting.Instrument.1" -SCRIPTING_ADV = "TEMAdvancedScripting.AdvancedInstrument.2" # .1 for Titan TEM server < 2.15 ? + +# Advanced scripting is available from TEM server 6.9 (Titan 2.9 / Talos 1.9) +SCRIPTING_ADV = "TEMAdvancedScripting.AdvancedInstrument.2" # .1 for TEM server < 6.15 ? + SCRIPTING_LOWDOSE = "LDServer.LdSrv" SCRIPTING_TIA = "ESVision.Application" -SCRIPTING_SEM_CCD = "SerialEMCCD.DMCamera" SCRIPTING_TECNAI_CCD = "TecnaiCCD.GatanCamera" SCRIPTING_TECNAI_CCD2 = "TecnaiCCDPlugin.GatanCamera" -SCRIPTING_FEI_GATAN_REMOTING = "ThermoScientificTEMPlugin.Acquisition" FEG_REGISTERS = "FegRegisters.FegReg" - -LICENSE_ADV = "TEMAdvancedScripting.AdvancedInstrumentInternal" -LICENSE_ADV_CAM = "TEMAdvancedScripting.CameraSettingsInternal" +CALGETTER = "CalGetter.Calibrations" HEADER_DATA = b'DT' HEADER_MSG = b'MS' diff --git a/pytemscript/utils/enums.py b/pytemscript/utils/enums.py index 78263b1..2c396fb 100644 --- a/pytemscript/utils/enums.py +++ b/pytemscript/utils/enums.py @@ -323,7 +323,7 @@ class LDState(IntEnum): FOCUS2 = 2 EXPOSURE = 3 -# ---------------- FEI Tecnai CCD enums ----------------------------------- +# ---------------- FEI Tecnai CCD enums --------------------------------------- class AcqSpeed(IntEnum): """ CCD acquisition mode. """ TURBO = 0 @@ -336,3 +336,143 @@ class AcqMode(IntEnum): SEARCH = 0 FOCUS = 1 RECORD = 2 + +# ----------------- CalGetter enums ------------------------------------------- +class CalibrationStatus(IntEnum): + NOT_CALIBRATED = 0 + INVALID_CALIBRATION = 1 + CALIBRATED = 2 + + +class CalibrationTypes(IntEnum): + MAGNIFICATION = 1 + BEAM_SHIFT = 2 + BEAM_TILT = 3 + IMAGE_SHIFT = 4 + DIFFRACTION_SHIFT = 5 + STAGE_SHIFT = 6 + FOCUS_STIGMATOR = 7 + ILLUMINATED_AREA = 8 + COUNT_TO_ELECTRONS = 9 + STAGE_TILT = 10 + FULL_STAGEX_LINEARIZATION = 11 + STEM_CALIBRATION = 12 + STEM_FOCUS_CALIBRATION = 13 + BEAM_TILT_AZIMUTH = 14 + STEM_HARDWARE_CORRECTION = 15 + + +class ModeTypes(IntEnum): + LM = 1 + MICROPROBE = 2 + NANOPROBE = 3 + LAD = 4 + MICROPROBE_D = 5 + NANOPROBE_D = 6 + LM_STEM = 7 + MICROPROBE_STEM = 8 + NANOPROBE_STEM = 9 + + +class LensSeriesTypes(IntEnum): + ZOOM = 1 + EFTEM = 2 + + +class LorentzTypes(IntEnum): + OFF = 1 + ON = 2 + + +class ActualMagnificationElements(IntEnum): + NOMINAL_MAGNIFICATION = 0 + CALIBRATED_MAGNIFICATION = 1 + MAGNIFICATION_INDEX = 2 + MAGNIFICATION_MODE = 3 + MAGNIFICATION_ROTATION = 4 + CERTIFIED = 5 + YEAR = 6 + MONTH = 7 + DAY = 8 + HOUR = 9 + MINUTE = 10 + SECOND = 11 + TOOLMATCH = 12 + BASE_MAGNIFICATION = 13 + + +class TransformTypes(IntEnum): + BEAM_SHIFT_LOG = 0 + BEAM_SHIFT_PHYS = 1 + BEAM_TILT_LOG = 2 + BEAM_TILT_PHYS = 3 + IMAGE_SHIFT_LOG = 4 + IMAGE_SHIFT_PHYS = 5 + DIFFRACTION_SHIFT_LOG = 6 + DIFFRACTION_SHIFT_PHYS = 7 + STAGE = 8 + STAGE_AS_SHIFT = 9 + STAGE_AS_TILT = 10 + + +class BasicTransformTypes(IntEnum): + PIXEL_TO_BEAMSHIFT = 0 + BEAMSHIFT_TO_PIXEL = 1 + BEAMSHIFT_LOG_TO_PHYS = 2 + BEAMSHIFT_PHYS_TO_LOG = 3 + PIXEL_TO_BEAMTILT = 4 + BEAMTILT_TO_PIXEL = 5 + BEAMTILT_LOG_TO_PHYS = 6 + BEAMTILT_PHYS_TO_LOG = 7 + PIXEL_TO_IMAGESHIFT = 8 + IMAGESHIFT_TO_PIXEL = 9 + IMAGESHIFT_LOG_TO_PHYS = 10 + IMAGESHIFT_PHYS_TO_LOG = 11 + PIXEL_TO_STAGESHIFT = 12 + STAGESHIFT_TO_PIXEL = 13 + IMAGESHIFT_TO_STAGESHIFT = 14 + STAGESHIFT_TO_IMAGESHIFT = 15 + PIXEL_TO_DIFFRACTIONSHIFT = 16 + DIFFRACTIONSHIFT_TO_PIXEL = 17 + DIFFRACTIONSHIFT_LOG_TO_PHYS = 18 + DIFFRACTIONSHIFT_PHYS_TO_LOG = 19 + BEAMSHIFT_TO_STAGESHIFT = 20 + STAGESHIFT_TO_BEAMSHIFT = 21 + PIXEL_TO_STAGETILT = 22 + STAGETILT_TO_PIXEL = 23 + BEAMTILT_TO_STAGETILT = 24 + STAGETILT_TO_BEAMTILT = 25 + DIFFRACTIONSHIFT_TO_STAGETILT = 26 + STAGETILT_TO_DIFFRACTIONSHIFT = 27 + PHYSICALPIXEL_TO_BEAMSHIFT = 28 + BEAMSHIFT_TO_PHYSICALPIXEL = 29 + PHYSICALPIXEL_TO_BEAMTILT = 30 + BEAMTILT_TO_PHYSICALPIXEL = 31 + PHYSICALPIXEL_TO_IMAGESHIFT = 32 + IMAGESHIFT_TO_PHYSICALPIXEL = 33 + PHYSICALPIXEL_TO_STAGESHIFT = 34 + STAGESHIFT_TO_PHYSICALPIXEL = 35 + PHYSICALPIXEL_TO_DIFFRACTIONSHIFT = 36 + DIFFRACTIONSHIFT_TO_PHYSICALPIXEL = 37 + PHYSICALPIXEL_TO_STAGETILT = 38 + STAGETILT_TO_PHYSICALPIXEL = 39 + BEAMSHIFT_TO_IMAGESHIFT = 40 + IMAGESHIFT_TO_BEAMSHIFT = 41 + BEAMTILT_TO_DIFFRACTIONSHIFT = 42 + DIFFRACTIONSHIFT_TO_BEAMTILT = 43 + CONDENSERSTIGMATOR_TO_PHYSICAL = 44 + PHYSICAL_TO_CONDENSERSTIGMATOR = 45 + OBJECTIVESTIGMATOR_TO_PHYSICAL = 46 + PHYSICAL_TO_OBJECTIVESTIGMATOR = 47 + DIFFRACTIONSTIGMATOR_TO_PHYSICAL = 48 + PHYSICAL_TO_DIFFRACTIONSTIGMATOR = 49 + PIXEL_TO_ALIGNBEAMSHIFT = 50 + ALIGNBEAMSHIFT_TO_PIXEL = 51 + ALIGNBEAMSHIFT_LOG_TOPHYS = 52 + ALIGNBEAMSHIFT_PHYS_TO_LOG = 53 + ALIGNBEAMSHIFT_TO_STAGESHIFT = 54 + STAGESHIFT_TO_ALIGNBEAMSHIFT = 55 + PHYSICALPIXEL_TO_ALIGNBEAMSHIFT = 56 + ALIGNBEAMSHIFT_TO_PHYSICALPIXEL = 57 + ALIGNBEAMSHIFT_TO_IMAGESHIFT = 58 + IMAGESHIFT_TO_ALIGNBEAMSHIFT = 59 diff --git a/pytemscript/utils/misc.py b/pytemscript/utils/misc.py index 60697ec..d593630 100644 --- a/pytemscript/utils/misc.py +++ b/pytemscript/utils/misc.py @@ -1,4 +1,5 @@ from typing import Optional, Any +import os import functools import numpy as np import logging @@ -13,7 +14,7 @@ def rgetattr(obj, attrname, *args, iscallable=False, log=True, **kwargs): """ Recursive getattr or callable on a COM object""" try: if log: - logging.debug("<= GET: %s, args=%s, kwargs=%s", + logging.debug("<= GET: %s, args=%r, kwargs=%r", attrname, args, kwargs) result = functools.reduce(getattr, attrname.split('.'), obj) return result(*args, **kwargs) if iscallable else result @@ -110,7 +111,9 @@ def convert_image(obj, bit_depth: Optional[int] = None, pixel_size: Optional[float] = None, advanced: Optional[bool] = False, - use_safearray: Optional[bool] = True): + use_safearray: Optional[bool] = True, + use_asfile: Optional[bool] = False, + **kwargs): """ Convert COM image object into an uint16 Image. :param obj: COM object @@ -121,14 +124,30 @@ def convert_image(obj, :param pixel_size: pixel size of the image :param advanced: advanced scripting flag :param use_safearray: use safearray method + :param use_asfile: use asfile method """ from pytemscript.modules import Image if use_safearray: + # Convert to a safearray and then to numpy from comtypes.safearray import safearray_as_ndarray with safearray_as_ndarray: - data = obj.AsSafeArray.astype("uint16") # AsSafeArray always returns int32 array + # AsSafeArray always returns int32 array + # Also, transpose is required to match TIA orientation + data = obj.AsSafeArray.astype("uint16").T + + elif use_asfile: + # Save into a temp file and read into numpy + import imageio + fn = r"C:/temp.tif" + if os.path.exists(fn): + os.remove(fn) + obj.SaveToFile(fn) if advanced else obj.AsFile(fn, 0) + data = imageio.imread(fn).astype("uint16") + os.remove(fn) + else: + # TecnaiCCD plugin: obj is a variant, convert to numpy data = np.array(obj, dtype="uint16") name = name or obj.Name @@ -161,9 +180,9 @@ def __init__(self, self.kwargs = kwargs def __str__(self) -> str: - return '{"attr": "%s", "validator": "%s", "kwargs": %s}' % ( + return '{"attr": "%s", "validator": "%s", "kwargs": %r}' % ( self.attr, self.validator, self.kwargs) def __repr__(self) -> str: - return 'RequestBody(attr=%s, validator=%s, kwargs=%s)' % ( + return 'RequestBody(attr=%s, validator=%s, kwargs=%r)' % ( self.attr, self.validator, self.kwargs) diff --git a/setup.py b/setup.py index 28a7b81..d59fed5 100644 --- a/setup.py +++ b/setup.py @@ -43,12 +43,12 @@ "mrcfile==1.3.0", "numpy==1.15.4", "pip<=19.1.1", - "pillow<=5.3.0", + "pillow==5.3.0", "setuptools<=12.0.5", "typing" ], extras_require={ - "extra": ["matplotlib", "mypy"], + "dev": ["matplotlib", "mypy"], "utapi": ["grpcio", "grpcio-tools", "protobuf"] }, entry_points={'console_scripts': [ diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 3e7a4d8..7044174 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -11,7 +11,7 @@ def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) from pytemscript.microscope import Microscope -from pytemscript.utils.enums import * +from pytemscript.utils.enums import AcqImageSize from pytemscript.modules.extras import Image @@ -129,21 +129,37 @@ def main(argv: Optional[List] = None) -> None: cameras = microscope.acquisition.cameras print("Available cameras:\n", cameras) - if microscope.optics.projection.is_eftem_on: - microscope.optics.projection.eftem_off() - - if "BM-Orius" in cameras: - camera_acquire(microscope, "BM-Orius", exp_time=0.25, binning=1) - if "BM-Ceta" in cameras: - camera_acquire(microscope, "BM-Ceta", exp_time=1, binning=2) - if "BM-Falcon" in cameras: - camera_acquire(microscope, "BM-Falcon", exp_time=0.5, binning=2) - camera_acquire(microscope, "BM-Falcon", exp_time=3, binning=1, - align_image=True, electron_counting=True, - save_frames=True, group_frames=2) - if "EF-CCD" in cameras: - microscope.optics.projection.eftem_on() - camera_acquire(microscope, "EF-CCD", exp_time=2, binning=1) + acq_params = { + "BM-Orius": {"exp_time": 0.25, "binning": 1}, + "BM-Ceta": {"exp_time": 1.0, "binning": 2}, + "BM-Falcon": {"exp_time": 0.5, "binning": 2}, + "EF-CCD": {"exp_time": 2.0, "binning": 1}, + } + acq_csa_params = { + "BM-Falcon": {"exp_time": 3.0, "binning": 1, "align_image": True, + "electron_counting": True, "save_frames": True, "group_frames": 2}, + "EF-Falcon": {"exp_time": 1.0, "binning": 1, + "electron_counting": True, "save_frames": True, "group_frames": 2}, + } + + for cam, cam_dict in cameras.items(): + if cam.startswith("BM-") and microscope.optics.projection.is_eftem_on: + microscope.optics.projection.eftem_off() + elif cam.startswith("EF-") and not microscope.optics.projection.is_eftem_on: + microscope.optics.projection.eftem_on() + + csa = cam_dict["supports_csa"] + if csa and cam in acq_csa_params: + csa_params = acq_csa_params[cam] + camera_acquire(microscope, cam, **csa_params) + + if cam_dict["supports_eer"]: + csa_params.pop("group_frames") + csa_params["eer"] = True + camera_acquire(microscope, cam, **csa_params) + + elif cam in acq_params: + camera_acquire(microscope, cam, **acq_params[cam]) if microscope.stem.is_available: microscope.stem.enable() diff --git a/tests/test_calgetter.py b/tests/test_calgetter.py new file mode 100644 index 0000000..8a60db6 --- /dev/null +++ b/tests/test_calgetter.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +import comtypes.client + +from pytemscript.utils.constants import CALGETTER +from pytemscript.utils.enums import ModeTypes, BasicTransformTypes, LensSeriesTypes +from pytemscript.plugins.calgetter import CalGetterPlugin + + +def main(): + """ Get existing calibrations from CalGetter. """ + try: + comtypes.CoInitialize() + obj = comtypes.client.CreateObject(CALGETTER) + except: + raise RuntimeError("Could not connect to %s interface" % CALGETTER) + + cg = CalGetterPlugin(obj) + assert cg.is_connected() + + cameras = [ + "BM-Orius", + "BM-Falcon", + "EF-Falcon" + ] + kv = 300 + + #camera = cg.get_reference_camera() + for camera in cameras: + print(camera) + print("Pixel size = ", cg.get_camera_pixel_size(camera)) + print("Camera rotation = ", cg.get_camera_rotation(camera)) + print("Image pixel size 50kx = ", cg.get_image_pixel_size(camera, + mode=ModeTypes.MICROPROBE, + magindex=29, + mag=50000.0, + kv=kv)) + print("Beam tilt log to phys", cg.basic_transform(BasicTransformTypes.BEAMTILT_LOG_TO_PHYS, + x=0.1, y=0.1)) + + modes = [ModeTypes.MICROPROBE, ModeTypes.NANOPROBE_STEM] + for mode in modes: + print(mode.name) + print("TEM mags:", cg.get_magnifications(camera, mode=mode, kv=kv)) + print("EFTEM mags:", cg.get_magnifications(camera, + mode=mode, + series=LensSeriesTypes.EFTEM, + kv=kv)) + print("\n") + + + +if __name__ == '__main__': + print("Testing CalGetter methods...") + main() diff --git a/tests/test_microscope.py b/tests/test_microscope.py index 0ea4790..4d7a1aa 100644 --- a/tests/test_microscope.py +++ b/tests/test_microscope.py @@ -34,12 +34,6 @@ def test_projection(microscope: Microscope, print("\tMagnification:", projection.magnification) print("\tMagnificationIndex:", projection.magnification_index) - # set ~ high SA mag - for mag in projection.list_magnifications.keys(): - if 30000 <= mag <= 60000: - projection.magnification = mag - break - projection.mode = ProjectionMode.DIFFRACTION print("\tCameraLength:", projection.camera_length) print("\tCameraLengthIndex:", projection.camera_length_index) @@ -178,11 +172,9 @@ def test_autoloader(microscope: Microscope, print(str(e)) -def test_stage(microscope: Microscope, - move_stage: bool = False) -> None: +def test_stage(microscope: Microscope) -> None: """ Test stage module attrs. :param microscope: Microscope object - :param move_stage: If true, move stage around """ stage = microscope.stage print("\nTesting stage...") @@ -192,9 +184,6 @@ def test_stage(microscope: Microscope, print("\tHolder:", stage.holder) print("\tLimits:", stage.limits) - if not move_stage: - return - print("Testing stage movement...") print("\tGoto(x=1, y=-1)") stage.go_to(x=1, y=-1) @@ -297,12 +286,10 @@ def test_stem(microscope: Microscope) -> None: def test_gun(microscope: Microscope, - has_gun1: bool = False, - has_feg: bool = False) -> None: + has_cfeg: bool = False) -> None: """ Test gun module attrs. :param microscope: Microscope object - :param has_gun1: If true, test GUN1 interface - :param has_feg: If true, test C-FEG interface + :param has_cfeg: If true, test C-FEG interface """ print("\nTesting gun...") gun = microscope.gun @@ -311,17 +298,23 @@ def test_gun(microscope: Microscope, print("\tShift:", gun.shift) print("\tTilt:", gun.tilt) - if has_gun1: - print("\tHighVoltageOffsetRange:", gun.voltage_offset_range) - print("\tHighVoltageOffset:", gun.voltage_offset) + try: + print("\tHVOffset:", gun.voltage_offset) + print("\tHVOffsetRange:", gun.voltage_offset_range) + except NotImplementedError: + pass - if has_feg: + if has_cfeg: print("\tFegState:", gun.feg_state) print("\tHTState:", gun.ht_state) print("\tBeamCurrent:", gun.beam_current) print("\tFocusIndex:", gun.focus_index) - gun.do_flashing(FegFlashingType.LOW_T) + try: + gun.do_flashing(FegFlashingType.LOW_T) + gun.do_flashing(FegFlashingType.HIGH_T) + except Warning: + pass def test_apertures(microscope: Microscope, @@ -346,25 +339,35 @@ def test_apertures(microscope: Microscope, aps.select("C2", 50) -def test_user_buttons(microscope: Microscope) -> None: - """ Test user button module attrs. +def test_energy_filter(microscope: Microscope) -> None: + """ Test energy filter module attrs. :param microscope: Microscope object """ - print("\nTesting user buttons...") - buttons = microscope.user_buttons - print("Buttons: %s" % buttons.show()) - import comtypes.client + if hasattr(microscope, "energy_filter"): + try: + print("\nTesting energy filter...") + ef = microscope.energy_filter + + print("\tZLPShift: ", ef.zlp_shift) + print("\tHTShift: ", ef.ht_shift) - def eventHandler(): - def Pressed(): - print("L1 button was pressed!") + ef.insert_slit(10) + print("\tSlit width: ", ef.slit_width) + ef.retract_slit() + except: + pass - buttons.L1.Assignment = "My function" - #comtypes.client.GetEvents(buttons.L1, eventHandler) - # Simulate L1 press - #buttons.L1.Pressed() - # Clear the assignment - buttons.L1.Assignment = "" + +def test_lowdose(microscope: Microscope) -> None: + """ Test LowDose module attrs. + :param microscope: Microscope object + """ + if hasattr(microscope, "low_dose") and microscope.low_dose.is_available: + print("\nTesting Low Dose...") + ld = microscope.low_dose + print("\tLowDose state: ", ld.state) + ld.on() + ld.off() def test_general(microscope: Microscope, @@ -384,7 +387,7 @@ def test_general(microscope: Microscope, else: assert microscope.condenser_system == CondenserLensSystem.TWO_CONDENSER_LENSES.name - if check_door: + if check_door and hasattr(microscope, "user_door"): print("\tUser door:", microscope.user_door.state) microscope.user_door.open() microscope.user_door.close() @@ -414,24 +417,21 @@ def main(argv: Optional[List] = None) -> None: print("Starting microscope tests, connection: %s" % args.type) - full_test = True test_projection(microscope, has_eftem=False) - test_vacuum(microscope, buffer_cycle=full_test) - test_autoloader(microscope, check_loading=full_test, slot=1) + test_vacuum(microscope, buffer_cycle=False) + test_autoloader(microscope, check_loading=False, slot=1) test_temperature(microscope, force_refill=False) - test_stage(microscope, move_stage=full_test) + test_stage(microscope) test_optics(microscope) test_illumination(microscope) - test_gun(microscope, has_gun1=False, has_feg=False) - if microscope.family != ProductFamily.TECNAI.name and args.type == "direct": - test_user_buttons(microscope) + test_gun(microscope, has_cfeg=False) + test_acquisition(microscope) + test_stem(microscope) + test_apertures(microscope, has_license=False) + test_energy_filter(microscope) + test_lowdose(microscope) test_general(microscope, check_door=False) - if full_test: - test_acquisition(microscope) - test_stem(microscope) - test_apertures(microscope, has_license=False) - microscope.disconnect() @@ -441,6 +441,6 @@ def main(argv: Optional[List] = None) -> None: """ Notes for Tecnai F20: -- DF element not found -> no DF mode or beam tilt. Check if python is 32-bit? +- DF element not found -> no DF mode or beam tilt. Python 32-bit issue? - Userbuttons not found """ diff --git a/tests/test_speed.py b/tests/test_speed.py new file mode 100644 index 0000000..daf6f3c --- /dev/null +++ b/tests/test_speed.py @@ -0,0 +1,42 @@ +from pytemscript.microscope import Microscope +from pytemscript.utils.enums import AcqImageSize +from pytemscript.modules.extras import Image + + +def acquire_image(microscope: Microscope, camera: str, **kwargs) -> Image: + image = microscope.acquisition.acquire_tem_image(camera, + size=AcqImageSize.FULL, + exp_time=3.0, + binning=1, + **kwargs) + return image + + +def main() -> None: + """ Testing acquisition speed. """ + microscope = Microscope(debug=True, useTecnaiCCD=False) + + print("Starting acquisition speed test") + cameras = microscope.acquisition.cameras + + for camera in ["BM-Ceta", "BM-Falcon", "EF-Falcon", "EF-CCD"]: + if camera in cameras: + + print("\tUsing SafeArray") + img1 = acquire_image(microscope, camera) + img1.save(r"C:/%s_safearray.mrc" % camera, overwrite=True) + + print("\tUsing AsFile") + # This should be 3x faster than SafeArray method above + img2 = acquire_image(microscope, camera, use_safearray=False, use_asfile=True) + img2.save(r"C:/%s_asfile.mrc" % camera, overwrite=True) + + if camera in ["EF-CCD", "BM-Orius"]: + print("\tUsing TecnaiCCD/TIA") + # This is faster than std scripting for Gatan CCD cameras + img3 = acquire_image(microscope, camera, use_tecnaiccd=True) + img3.save(r"C:/%s_tecnaiccd.mrc" % camera, overwrite=True) + + +if __name__ == '__main__': + main()