diff --git a/.github/workflows/publish_and_tag.yml b/.github/workflows/publish_and_tag.yml index 93e06eb..9e941e1 100644 --- a/.github/workflows/publish_and_tag.yml +++ b/.github/workflows/publish_and_tag.yml @@ -12,6 +12,9 @@ env: jobs: deploy: runs-on: ubuntu-latest + permissions: + # IMPORTANT: this permission is mandatory for Trusted Publishing + id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -24,13 +27,10 @@ jobs: - name: Check extra requirements if: ${{ hashFiles('requirements.txt') != '' }} run: pip install -r requirements.txt - - name: publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - python -m build --no-isolation - twine upload dist/* + - name: Build dist + run: python -m build --no-isolation + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 - name: Get version and tag run: | export PACKAGE_VERSION=$(python -c "import $FOLDER_WITH_VERSION; print('VERSION', 'v'+$FOLDER_WITH_VERSION.__version__)" | grep VERSION | sed "s/VERSION //g") diff --git a/README.rst b/README.rst index fd814bb..d0edd9a 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,8 @@ New changes and this whole product is distributed under either version 3 of the Documentation ------------- +The source code is available at https://github.com/azazellochg/pytemscript + The documentation can be found at https://pytemscript.readthedocs.io Quick example diff --git a/docs/acquisition.rst b/docs/acquisition.rst new file mode 100644 index 0000000..c38fe22 --- /dev/null +++ b/docs/acquisition.rst @@ -0,0 +1,107 @@ +Image acquisition +================= + +The acquisition can become quite cumbersome due to many different cameras from various manufacturers installed on a microscope. +Here we describe supported / tested cameras and explain how they can be controlled by ``pytemscript`` + +List of tested cameras: + + * Orius CCD (SC200W (830), SC200B (830)) + * Ceta 16M + * Ceta D + * Falcon 3EC + * Falcon 4(i) + * K2 + * K3 + +All methods described below return a 16-bit unsigned integer (equivalent to MRC mode 6) :meth:`~pytemscript.modules.Image` object. +If movies are being acquired asynchronously, their format can be different. + +Standard scripting +------------------ + +Gatan CCD cameras are usually embedded by TFS and can be controlled via standard scripting. This requires both Digital Micrograph +and TIA to be opened as well as the current camera selected in the Microscope User Interface (CCD/TV camera panel). + +.. code-block:: python + + microscope = Microscope() + acq = microscope.acquisition + img = acq.acquire_tem_image("BM-Orius", AcqImageSize.FULL, exp_time=1.0, binning=2) + +.. warning:: If you need to change the camera, after doing so in the Microscope interface, you have to reconnect the microscope client since the COM interface needs to be reinitialised. + +For Gatan K2/K3 cameras (if they are embedded by TFS), standard scripting can only return unaligned average image, +there are no options to acquire movies or change the mode (linear/counting). +You can only modify binning or exposure time. + +TecnaiCCD plugin +---------------- + +FEI has created their own plugin for Gatan CCD cameras. The plugin needs to be installed on Gatan PC inside Digital Micrograph. +Digital Micrograph and TIA need to be opened as well as the current camera selected in the Microscope User Interface (CCD/TV camera panel). +The advantage of this method over standard scripting is ~20 % speed improvement for both acquisition and image return, because the plugin +interacts directly with Digital Micrograph and does not return the image to TIA. + +.. code-block:: python + + microscope = Microscope(useTecnaiCCD=True) + acq = microscope.acquisition + img = acq.acquire_tem_image("BM-Orius", AcqImageSize.FULL, exp_time=1.0, binning=2, use_tecnaiccd=True) + +SerialEMCCD plugin +------------------ + +David Mastronarde has created a SerialEM `plugin `_ to control both Gatan CCDs and advanced cameras like K2 or K3. +The plugin has to be installed on Gatan PC inside Digital Micrograph, which is normally done during SerialEM installation. +The connection to the plugin is established via a socket interface created by ``pytemscript`` (same way as Leginon does it). +Digital Micrograph needs to be opened. SerialEM does not have to be running. + +The plugin provides multiple options for movie acquisition, frame alignment etc. + +.. warning:: In development, not available yet + +Advanced scripting +------------------ + +This scripting interface was developed by TFS for their newer cameras like Ceta and Falcon. +The currently supported cameras are Ceta 1, Ceta 2, Falcon 3 and Falcon 4(i). +The interface includes new features like movie acquisition, counting mode, EER format etc. +Movies are offloaded asynchronously to the storage server, while the returned image is an average (aligned or not). + +There's no need to open TIA or select the camera in the microscope interface. + +See details for :meth:`~pytemscript.modules.Acquisition.acquire_tem_image` + +.. code-block:: python + + microscope = Microscope() + acq = microscope.acquisition + img = acq.acquire_tem_image("BM-Falcon", AcqImageSize.FULL, exp_time=5.0, binning=1, electron_counting=True, align_image=True, group_frames=2) + +.. note:: Advanced scripting features like "Camera Electron Counting" and "Camera Dose Fractions" require separate licenses from TFS. + +Speed up the acquisition +------------------------ + +By default, ``pytemscript`` will use `AsSafeArray` method to convert the COM image object to a numpy array via standard or advanced scripting. +Depending on the image size this method can be very slow (several seconds). There's a trick to save the image object to a temporary file +(`AsFile` COM method) and then read it, which seems to work much faster (up to 3x). However, this requires an extra `imageio` dependency for reading the temporary file. + +.. warning:: On some systems, saving to a file can fail with a COM error due to incomplete implementation, so you will have to stick to the default `AsSafeArray` method. + +If you want to try this method, add a couple of kwargs to your acquisition command: + +.. code-block:: python + + microscope = Microscope() + acq = microscope.acquisition + img = acq.acquire_tem_image("BM-Falcon", AcqImageSize.FULL, exp_time=5.0, use_safearray=False, use_asfile=True) + + +STEM acquisition +---------------- + +STEM detectors have to be embedded by FEI and selected in the Microscope User Interface (STEM user panel). They are controlled by standard scripting. + +.. note:: Be aware that the acquisition starts immediately without waiting for STEM detectors insertion to finish. It's probably better to manually insert them first in the microscope interface. diff --git a/docs/changelog.rst b/docs/changelog.rst index b8a60f3..1f7120e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,11 +23,6 @@ Version 3.0 - Tundra (Win10, Python 3.11) - Titan Krios G1 (Win7, Python 3.6), G2, G3i (Win10, Python 3.8), G4 (Win10, Python 3.8) -* Future plans: - - - UTAPI client - - Acquisition series - Version 2.0.0 ^^^^^^^^^^^^^ diff --git a/docs/components/events.rst b/docs/components/events.rst index 44c5125..11228ac 100644 --- a/docs/components/events.rst +++ b/docs/components/events.rst @@ -5,7 +5,7 @@ You can receive events from hand panel buttons when using the local client on th can be assigned with a custom Python function that will be executed upon pressing. We provide the :meth:`~pytemscript.modules.ButtonHandler` class that takes care of assigning events. -.. warning:: Don't forget to clear the custom button assignment at the end using `clear()` method. This will restore the previous assignment. +.. note:: Don't forget to clear the custom button assignment at the end using `clear()` method. This will restore the previous assignment. See example below: diff --git a/docs/components/index.rst b/docs/components/index.rst index a061e81..29e49b1 100644 --- a/docs/components/index.rst +++ b/docs/components/index.rst @@ -71,7 +71,8 @@ Vectors ------- Some attributes handle two dimensional vectors that have X and Y values (e.g. image shift or gun tilt). These -attributes accept and return a :meth:`~pytemscript.modules.Vector` of two floats. Vectors can be multiplied, subtracted etc.: +attributes accept and return a :meth:`~pytemscript.modules.Vector` of two floats. Vectors can be multiplied, subtracted etc. as shown below. +You can also use a list or a tuple to set vector attributes. .. code-block:: python @@ -80,6 +81,8 @@ attributes accept and return a :meth:`~pytemscript.modules.Vector` of two floats shift += (0.4, 0.2) shift *= 2 microscope.optics.illumination.beam_shift = shift + projection.image_shift = (0.05, 0.1) + projection.image_shift = [0.05, 0.1] .. autoclass:: pytemscript.modules.Vector :members: set_limits, check_limits, get, set diff --git a/docs/conf.py b/docs/conf.py index 8a0ca88..ea1f0b6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ author = 'Tore Niermann, Grigory Sharov' # The full version, including alpha/beta/rc tags -release = '3.0b1' +release = '3.0b3' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. diff --git a/docs/index.rst b/docs/index.rst index d1a3150..6c4922a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,8 @@ New changes and this whole product is distributed under either version 3 of the Documentation ------------- +The source code is available at https://github.com/azazellochg/pytemscript + The documentation can be found at https://pytemscript.readthedocs.io .. toctree:: @@ -27,6 +29,7 @@ The documentation can be found at https://pytemscript.readthedocs.io installation components/index getting_started + acquisition remote changelog diff --git a/docs/installation.rst b/docs/installation.rst index 39653c7..d54f379 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,13 +1,22 @@ Installation ============ -Requirements: +Prerequisites for the FEI or Thermo Fisher Scientific microscope: + + * TEM Scripting + * TEM Advanced scripting (optional) + * LowDose (optional) + * TecnaiCCD plugin for Digital Micrograph (optional) + * SerialEMCCD plugin for Digital Micrograph (optional) + +Requirements for this package: * python 3.4 or newer * comtypes * mrcfile (to save MRC files) * numpy * pillow (to save non-MRC files) + * imageio (optional, to speed up image acquisition) Online installation on Windows ############################## diff --git a/docs/remote.rst b/docs/remote.rst index 34f2eef..6ebf4f1 100644 --- a/docs/remote.rst +++ b/docs/remote.rst @@ -12,7 +12,7 @@ Socket-based client In this mode the pytemscript socket server must run on the microscope PC (Windows). By default, it will listen for clients on port 39000. -.. warning:: +.. danger:: The server provides no means of security or authorization control itself. Thus it is highly recommended to let the server only listen to internal networks or at least route it through a reverse proxy, which implements sufficient security. @@ -42,7 +42,7 @@ 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. +To shutdown pytemscript-server, press Ctrl+C in the server console. UTAPI client ------------ @@ -55,7 +55,8 @@ 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. -Pytemscript converts its API commands to UTAPI calls. The client requires extra dependencies to be installed: +Pytemscript converts its API commands to UTAPI calls. The client only supports Python 3.8+ and requires +a few extra dependencies to be installed: .. code-block:: python diff --git a/pytemscript/__init__.py b/pytemscript/__init__.py index 545f63d..6feaacf 100644 --- a/pytemscript/__init__.py +++ b/pytemscript/__init__.py @@ -1 +1 @@ -__version__ = '3.0b2' +__version__ = '3.0b3' diff --git a/pytemscript/microscope.py b/pytemscript/microscope.py index c94dc8f..645beb2 100644 --- a/pytemscript/microscope.py +++ b/pytemscript/microscope.py @@ -59,7 +59,7 @@ def __init__(self, connection: str = "direct", *args, **kwargs) -> None: self.apertures = Apertures(client) self.temperature = Temperature(client) - if connection == "direct" and self.family != ProductFamily.TECNAI.name: + if connection == "direct": self.user_buttons = UserButtons(client) if client.has_advanced_iface: @@ -89,6 +89,5 @@ def condenser_system(self) -> str: return CondenserLensSystem(result).name def disconnect(self) -> None: - """ Disconnects the remote client. Not applicable for direct connection.""" - if self.__connection != "direct": - self.__client.disconnect() + """ Disconnects the client. """ + self.__client.disconnect() diff --git a/pytemscript/modules/acquisition.py b/pytemscript/modules/acquisition.py index 02fc0bf..9836d2d 100644 --- a/pytemscript/modules/acquisition.py +++ b/pytemscript/modules/acquisition.py @@ -5,7 +5,7 @@ from functools import lru_cache from ..utils.misc import RequestBody, convert_image -from ..utils.enums import AcqImageSize, AcqShutterMode, PlateLabelDateFormat, ScreenPosition +from ..utils.enums import AcqImageSize, AcqShutterMode, AcqImageCorrection, PlateLabelDateFormat, ScreenPosition from .extras import Image, SpecialObj @@ -166,7 +166,7 @@ def restore_shutter(self, def set_tem_presets(self, cameraName: str, size: AcqImageSize = AcqImageSize.FULL, - exp_time: float = 1, + exp_time: float = 1.0, binning: int = 1, **kwargs) -> Optional[int]: @@ -213,7 +213,7 @@ def set_tem_presets(self, def set_tem_presets_advanced(self, cameraName: str, size: AcqImageSize = AcqImageSize.FULL, - exp_time: float = 1, + exp_time: float = 1.0, binning: int = 1, use_cca: bool = False, **kwargs) -> None: @@ -351,16 +351,7 @@ def set_stem_presets(self, class Acquisition: - """ Image acquisition functions. - - In order for acquisition to be available TIA (TEM Imaging and Acquisition) - must be running (even if you are using DigitalMicrograph as the CCD server). - - If it is necessary to update the acquisition object (e.g. when the STEM detector - selection on the TEM UI has been changed), you have to release and recreate the - main microscope object. If you do not do so, you keep accessing the same - acquisition object which will not work properly anymore. - """ + """ Image acquisition functions. """ __slots__ = ("__client", "__id_adv") def __init__(self, client): @@ -464,7 +455,7 @@ def __acquire_with_tecnaiccd(self, def acquire_tem_image(self, cameraName: str, size: AcqImageSize = AcqImageSize.FULL, - exp_time: float = 1, + exp_time: float = 1.0, binning: int = 1, **kwargs) -> Optional[Image]: """ Acquire a TEM image. @@ -476,6 +467,11 @@ def acquire_tem_image(self, :param exp_time: Exposure time in seconds :type exp_time: float :param binning: Binning factor + :keyword IntEnum correction: Image correction (AcqImageCorrection enum) + :keyword IntEnum exposure_mode: CCD exposure mode (AcqExposureMode enum) + :keyword IntEnum shutter_mode: CCD shutter mode (AcqShutterMode enum) + :keyword IntEnum pre_exp_time: The pre-exposure time in seconds. + :keyword IntEnum pre_exp_pause_time: The time delay after pre-exposure and before the actual CCD exposure in seconds. :keyword bool align_image: Whether frame alignment (i.e. drift correction) is to be applied to the final image as well as the intermediate images. Advanced cameras only. :keyword bool electron_counting: Use counting mode. Advanced cameras only. :keyword bool eer: Use EER mode. Advanced cameras only. @@ -485,6 +481,14 @@ def acquire_tem_image(self, :keyword bool use_tecnaiccd: Use Tecnai CCD plugin to acquire image via Digital Micrograph, only for Gatan cameras. Requires Microscope() initialized with useTecnaiCCD=True :returns: Image object + Extra notes: + + - Keyword arguments correction, exposure_mode, shutter_mode, pre_exp_time, pre_exp_pause_time are only available for CCD cameras that use standard scripting. + - Advanced cameras are Ceta 1, Ceta 2, Falcon 3, Falcon 4(i). + - Counting mode and frame saving requires a separate license enabled in TEM software. + - Continuous acquisition with recording is supported only by Ceta 2. + - TecnaiCCD plugin is only available for Gatan CCD cameras. + Usage: >>> microscope = Microscope() >>> acq = microscope.acquisition @@ -569,7 +573,7 @@ def acquire_tem_image(self, def acquire_stem_image(self, cameraName: str, - size: AcqImageSize, + size: AcqImageSize = AcqImageSize.FULL, dwell_time: float = 1e-5, binning: int = 1, **kwargs) -> Image: @@ -581,10 +585,10 @@ def acquire_stem_image(self, :type size: IntEnum :param dwell_time: Dwell time in seconds. The frame time equals the dwell time times the number of pixels plus some overhead (typically 20%, used for the line flyback) :type dwell_time: float - :param binning: Binning factor + :param binning: Binning factor. Technically speaking these are "pixel skipping" values, since in STEM we do not combine pixels as a CCD does. :type binning: int - :keyword float brightness: Brightness setting - :keyword float contrast: Contrast setting + :keyword float brightness: Brightness setting (0.0-1.0) + :keyword float contrast: Contrast setting (0.0-1.0) :returns: Image object """ _ = self.__find_camera(cameraName, self.stem_detectors, binning) @@ -627,8 +631,8 @@ def acquire_film(self, body = RequestBody(attr="tem.Camera", obj_cls=AcquisitionObj, obj_method="acquire_film", - film_text = film_text, - exp_time = exp_time) + film_text=film_text, + exp_time=exp_time) self.__client.call(method="exec_special", body=body) logging.info("Film exposure completed") else: @@ -637,7 +641,7 @@ def acquire_film(self, @property def film_settings(self) -> Dict: """ Returns a dict with film settings. - Note: The plate camera has become obsolete with Win7 so + Note: The plate camera has become obsolete with Windows 7 so most of the existing functions are no longer supported. """ if self.__has_film: @@ -651,15 +655,15 @@ def film_settings(self) -> Dict: return {} @property - def screen(self) -> str: + def screen_position(self) -> str: """ Fluorescent screen position. (read/write)""" body = RequestBody(attr="tem.Camera.MainScreen", validator=int) result = self.__client.call(method="get", body=body) return ScreenPosition(result).name - @screen.setter - def screen(self, value: ScreenPosition) -> None: + @screen_position.setter + def screen_position(self, value: ScreenPosition) -> None: body = RequestBody(attr="tem.Camera.MainScreen", value=value) self.__client.call(method="set", body=body) @@ -676,7 +680,10 @@ def stem_detectors(self) -> Dict: @property @lru_cache(maxsize=1) def cameras(self) -> Dict: - """ Returns a dict with parameters for all TEM cameras. """ + """ Returns a dict with parameters for all TEM cameras. + supports_csa = single acquisition (Ceta 1, Ceta 2, Falcon 3, Falcon 4(i)) + supports_cca = continuous acquisition (Ceta 2 only) + """ body = RequestBody(attr="tem.Acquisition.Cameras", validator=dict, obj_cls=AcquisitionObj, @@ -686,7 +693,7 @@ def cameras(self) -> Dict: if not self.__client.has_advanced_iface: return tem_cameras - # CSA is supported by Ceta 1, Ceta 2, Falcon 3, Falcon 4 + # CSA is supported by Ceta 1, Ceta 2, Falcon 3, Falcon 4(i) body = RequestBody(attr=self.__id_adv + ".CameraSingleAcquisition", validator=dict, obj_cls=AcquisitionObj, diff --git a/pytemscript/modules/apertures.py b/pytemscript/modules/apertures.py index 18da059..23b4a72 100644 --- a/pytemscript/modules/apertures.py +++ b/pytemscript/modules/apertures.py @@ -4,7 +4,7 @@ from .extras import SpecialObj from ..utils.misc import RequestBody -from ..utils.enums import MechanismId, MechanismState +from ..utils.enums import MechanismId, MechanismState, ApertureType class AperturesObj(SpecialObj): @@ -17,45 +17,45 @@ def show(self) -> Dict: apertures[MechanismId(ap.Id).name] = { "retractable": ap.IsRetractable, "state": MechanismState(ap.State).name, - "sizes": [a.Diameter for a in ap.ApertureCollection] + "sizes": [int(a.Diameter) for a in ap.ApertureCollection], + "types": [ApertureType(a.Type).name for a in ap.ApertureCollection], } return apertures - def _find_aperture(self, name: str): + def _find_aperture(self, name: MechanismId): """ Helper method to find the aperture object by name. """ - name = name.upper() for ap in self.com_object: - if name == MechanismId(ap.Id).name: + if name == MechanismId(ap.Id): return ap - raise KeyError("No aperture with name %s" % name) + raise KeyError("No aperture with name %s" % name.name) - def enable(self, name: str) -> None: + def enable(self, name: MechanismId) -> None: ap = self._find_aperture(name) ap.Enable() - def disable(self, name: str) -> None: + def disable(self, name: MechanismId) -> None: ap = self._find_aperture(name) ap.Disable() - def retract(self, name: str) -> None: + def retract(self, name: MechanismId) -> None: ap = self._find_aperture(name) if ap.IsRetractable: ap.Retract() else: - raise NotImplementedError("Aperture %s is not retractable" % name) + raise NotImplementedError("Aperture %s is not retractable" % name.name) - def select(self, name: str, size: int) -> None: + def select(self, name: MechanismId, size: int) -> None: ap = self._find_aperture(name) if ap.State == MechanismState.DISABLED: ap.Enable() for a in ap.ApertureCollection: - if a.Diameter == size: + if int(a.Diameter) == size: ap.SelectAperture(a) - if ap.SelectedAperture.Diameter == size: + if int(ap.SelectedAperture.Diameter) == size: return else: - raise RuntimeError("Could not select aperture!") + raise RuntimeError("Could not select aperture %s=%d" % (name.name, size)) class Apertures: @@ -98,7 +98,7 @@ def vpp_next_position(self) -> None: except: raise RuntimeError(self.__err_msg_vpp) - def enable(self, aperture) -> None: + def enable(self, aperture: MechanismId) -> None: if not self.__std_available: raise NotImplementedError(self.__err_msg) else: @@ -106,7 +106,7 @@ def enable(self, aperture) -> None: obj_method="enable", name=aperture) self.__client.call(method="exec_special", body=body) - def disable(self, aperture) -> None: + def disable(self, aperture: MechanismId) -> None: if not self.__std_available: raise NotImplementedError(self.__err_msg) else: @@ -114,7 +114,7 @@ def disable(self, aperture) -> None: obj_method="disable", name=aperture) self.__client.call(method="exec_special", body=body) - def retract(self, aperture) -> None: + def retract(self, aperture: MechanismId) -> None: if not self.__std_available: raise NotImplementedError(self.__err_msg) else: @@ -122,13 +122,13 @@ def retract(self, aperture) -> None: obj_method="retract", name=aperture) self.__client.call(method="exec_special", body=body) - def select(self, aperture: str, size: int) -> None: + def select(self, aperture: MechanismId, size: int) -> None: """ Select a specific aperture. - :param aperture: Aperture name (C1, C2, C3, OBJ or SA) - :type aperture: str + :param aperture: Aperture name (MechanismId enum) + :type aperture: MechanismId :param size: Aperture size - :type size: float + :type size: int """ if not self.__std_available: raise NotImplementedError(self.__err_msg) diff --git a/pytemscript/modules/energyfilter.py b/pytemscript/modules/energyfilter.py index af3c653..a5c7280 100644 --- a/pytemscript/modules/energyfilter.py +++ b/pytemscript/modules/energyfilter.py @@ -58,7 +58,7 @@ def retract_slit(self) -> None: @property def slit_width(self) -> float: - """ Returns energy slit width in eV. """ + """ Energy slit width in eV. (read/write)""" if not self.__has_ef: raise NotImplementedError(self.__err_msg) @@ -76,7 +76,7 @@ def slit_width(self, value: float) -> None: @property def ht_shift(self) -> float: - """ Returns High Tension energy shift in eV. """ + """ High Tension energy shift in eV. (read/write)""" if not self.__has_ef: raise NotImplementedError(self.__err_msg) @@ -94,7 +94,7 @@ def ht_shift(self, value: float) -> None: @property def zlp_shift(self) -> float: - """ Returns Zero-Loss Peak (ZLP) energy shift in eV. """ + """ Zero-Loss Peak (ZLP) energy shift in eV. (read/write)""" if not self.__has_ef: raise NotImplementedError(self.__err_msg) diff --git a/pytemscript/modules/extras.py b/pytemscript/modules/extras.py index ea6f640..bd0420c 100644 --- a/pytemscript/modules/extras.py +++ b/pytemscript/modules/extras.py @@ -1,19 +1,14 @@ -from typing import Optional, Dict, Tuple, Union +from typing import Optional, Dict, Tuple, Union, List from datetime import datetime import math import logging +import os.path from pathlib import Path import numpy as np from functools import lru_cache from collections import OrderedDict - -try: - import PIL.Image as PilImage - import PIL.TiffImagePlugin as PilTiff -except ImportError: - print("Pillow library not found, you won't be able to " - "save images in non-MRC format.") - +import PIL.Image as PilImage +import PIL.TiffImagePlugin as PilTiff from ..utils.enums import StageAxes, MeasurementUnitType @@ -51,6 +46,16 @@ def __repr__(self) -> str: def __str__(self): return "(%f, %f)" % (self.x, self.y) + @classmethod + def convert_to(cls, value: Union[Tuple[float, float], List[float], "Vector"]): + """ Convert input value into a Vector. """ + if isinstance(value, (tuple, list)): + return cls(x=value[0], y=value[1]) + elif isinstance(value, cls): + return value + else: + raise TypeError("Expected a tuple, list or another Vector") + def set_limits(self, min_value: float, max_value: float) -> None: """Set the range limits for the vector for both X and Y.""" self.__min = min_value @@ -73,12 +78,14 @@ def get(self) -> Tuple: """Return the vector components as a tuple.""" return self.x, self.y - def set(self, value: Tuple[float, float]) -> None: - """ Update values from a tuple. """ - if not isinstance(value, tuple): - raise TypeError("Expected a tuple of floats") + def set(self, value: Union[Tuple[float, float], List[float], "Vector"]) -> None: + """ Update current values from a tuple, list or another Vector. """ + if isinstance(value, (tuple, list)): + self.x, self.y = value[0], value[1] + elif isinstance(value, self.__class__): + self.x, self.y = value.x, value.y else: - self.x, self.y = value + raise TypeError("Expected a tuple, list or another Vector") def __add__(self, other: Union['Vector', Tuple]) -> 'Vector': if isinstance(other, tuple): @@ -125,7 +132,7 @@ def __neg__(self) -> 'Vector': class Image: """ Acquired image basic object. - :param data: int16 numpy array + :param data: uint16 numpy array :type data: numpy.ndarray :param name: name of the image :type name: str @@ -193,17 +200,15 @@ def __create_tiff_tags(self): def save(self, fn: Union[Path, str], overwrite: bool = False) -> None: - """ Save acquired image to a file as int16. + """ Save acquired image to a file as uint16. Supported formats: mrc, tiff, tif, png. To save in non-mrc format you will need pillow package installed. :param fn: File path :param overwrite: Overwrite existing file """ - if isinstance(fn, str): - fn = Path(fn) - - ext = fn.suffix.lower() + fn = os.path.abspath(fn) + ext = os.path.splitext(fn)[-1].lower() if ext == ".mrc": import mrcfile @@ -213,8 +218,8 @@ def save(self, mrc.set_data(self.data) elif ext in [".tiff", ".tif", ".png"]: - if fn.exists() and not overwrite: - raise FileExistsError("File %s already exists, use overwrite flag" % fn.resolve()) + if os.path.exists(fn) and not overwrite: + raise FileExistsError("File %s already exists, use overwrite flag" % fn) logging.getLogger("PIL").setLevel(logging.INFO) pil_image = PilImage.fromarray(self.data, mode='I;16') @@ -224,7 +229,7 @@ def save(self, else: raise NotImplementedError("Unsupported file format: %s" % ext) - logging.info("File saved: %s", fn.resolve()) + logging.info("File saved: %s", fn) class SpecialObj: diff --git a/pytemscript/modules/gun.py b/pytemscript/modules/gun.py index 06a8dd4..d56f88a 100644 --- a/pytemscript/modules/gun.py +++ b/pytemscript/modules/gun.py @@ -1,7 +1,7 @@ from functools import lru_cache import logging import time -from typing import Tuple +from typing import Tuple, Union, List from ..utils.misc import RequestBody from ..utils.enums import FegState, HighTensionState, FegFlashingType @@ -80,10 +80,11 @@ def shift(self) -> Vector: return Vector(x, y) @shift.setter - def shift(self, vector: Vector) -> None: - vector.set_limits(-1.0, 1.0) + def shift(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) + value.set_limits(-1.0, 1.0) - body = RequestBody(attr=self.__id + ".Shift", value=vector) + body = RequestBody(attr=self.__id + ".Shift", value=value) self.__client.call(method="set", body=body) @property @@ -98,10 +99,11 @@ def tilt(self) -> Vector: return Vector(x, y) @tilt.setter - def tilt(self, vector: Vector) -> None: - vector.set_limits(-1.0, 1.0) + def tilt(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) + value.set_limits(-1.0, 1.0) - body = RequestBody(attr=self.__id + ".Tilt", value=vector) + body = RequestBody(attr=self.__id + ".Tilt", value=value) self.__client.call(method="set", body=body) @property @@ -172,7 +174,7 @@ def voltage(self) -> float: @voltage.setter def voltage(self, value: float) -> None: voltage_max = self.voltage_max - if not (0.0 <= value <= voltage_max): + if not (0.0 <= float(value) <= voltage_max): raise ValueError("%s is outside of range 0.0-%s" % (value, voltage_max)) body = RequestBody(attr=self.__id + ".HTValue", value=float(value) * 1000) @@ -223,8 +225,8 @@ def extractor_voltage(self) -> float: raise NotImplementedError(self.__err_msg_cfeg) @property - def focus_index(self) -> Tuple[int, int]: - """ Returns coarse and fine gun lens index. """ + def gun_lens(self) -> Tuple[int, int]: + """ Returns coarse and fine gun lens index. Not available on systems with a monochromator. """ if self.__has_source: coarse = RequestBody(attr=self.__id_adv + ".FocusIndex.Coarse", validator=int) fine = RequestBody(attr=self.__id_adv + ".FocusIndex.Fine", validator=int) @@ -242,12 +244,23 @@ def do_flashing(self, flash_type: FegFlashingType) -> None: if not self.__has_source: raise NotImplementedError(self.__err_msg_cfeg) - body = RequestBody(attr=self.__id_adv + ".Flashing.IsFlashingAdvised()", - arg=flash_type, validator=bool) - if self.__client.call(method="exec", body=body): + if self.is_flashing_advised(flash_type): # Warning: lowT flashing can be done even if not advised doflash = RequestBody(attr=self.__id_adv + ".Flashing.PerformFlashing()", arg=flash_type) self.__client.call(method="exec", body=doflash) else: raise Warning("Flashing type %s is not advised" % flash_type) + + def is_flashing_advised(self, flash_type: FegFlashingType) -> bool: + """ Check if cold FEG flashing is advised. + + :param flash_type: FEG flashing type (FegFlashingType enum) + :type flash_type: IntEnum + """ + if not self.__has_source: + raise NotImplementedError(self.__err_msg_cfeg) + + body = RequestBody(attr=self.__id_adv + ".Flashing.IsFlashingAdvised()", + arg=flash_type, validator=bool) + return self.__client.call(method="exec", body=body) diff --git a/pytemscript/modules/illumination.py b/pytemscript/modules/illumination.py index ea33922..69e8012 100644 --- a/pytemscript/modules/illumination.py +++ b/pytemscript/modules/illumination.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, List, Tuple import math from .extras import Vector @@ -90,9 +90,9 @@ def beam_shift(self) -> Vector: return Vector(x, y) * 1e6 @beam_shift.setter - def beam_shift(self, vector: Vector) -> None: - vector *= 1e-6 - body = RequestBody(attr=self.__id + ".Shift", value=vector) + def beam_shift(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) * 1e-6 + body = RequestBody(attr=self.__id + ".Shift", value=value) self.__client.call(method="set", body=body) @property @@ -110,9 +110,9 @@ def rotation_center(self) -> Vector: return Vector(x, y) * 1e3 @rotation_center.setter - def rotation_center(self, vector: Vector) -> None: - vector *= 1e-3 - body = RequestBody(attr=self.__id + ".RotationCenter", value=vector) + def rotation_center(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) * 1e-3 + body = RequestBody(attr=self.__id + ".RotationCenter", value=value) self.__client.call(method="set", body=body) @property @@ -125,31 +125,36 @@ def condenser_stigmator(self) -> Vector: self.__client.call(method="get", body=stigy)) @condenser_stigmator.setter - def condenser_stigmator(self, vector: Vector) -> None: - vector.set_limits(-1.0, 1.0) - body = RequestBody(attr=self.__id + ".CondenserStigmator", value=vector) + def condenser_stigmator(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) + value.set_limits(-1.0, 1.0) + body = RequestBody(attr=self.__id + ".CondenserStigmator", value=value) self.__client.call(method="set", body=body) @property def illuminated_area(self) -> float: """ Illuminated area in um. Works only on 3-condenser lens systems. (read/write)""" - if self.__has_3cond: + if not self.__has_3cond: + raise NotImplementedError("Illuminated area exists only on 3-condenser lens systems.") + if self.condenser_mode == CondenserMode.PARALLEL.name: body = RequestBody(attr=self.__id + ".IlluminatedArea", validator=float) return self.__client.call(method="get", body=body) * 1e6 else: - raise NotImplementedError("Illuminated area exists only on 3-condenser lens systems.") + raise RuntimeError("Condenser is not in Parallel mode.") @illuminated_area.setter def illuminated_area(self, value: float) -> None: - if self.__has_3cond: + if not self.__has_3cond: + raise NotImplementedError("Illuminated area exists only on 3-condenser lens systems.") + if self.condenser_mode == CondenserMode.PARALLEL.name: body = RequestBody(attr=self.__id + ".IlluminatedArea", value=value*1e-6) self.__client.call(method="set", body=body) else: - raise NotImplementedError("Illuminated area exists only on 3-condenser lens systems.") + raise RuntimeError("Condenser is not in Parallel mode.") @property def probe_defocus(self) -> float: - """ Probe defocus. Works only on 3-condenser lens systems in probe mode. (read/write)""" + """ Probe defocus. Works only on 3-condenser lens systems in probe mode. """ if not self.__has_3cond: raise NotImplementedError("Probe defocus exists only on 3-condenser lens systems.") if self.condenser_mode == CondenserMode.PROBE.name: @@ -158,19 +163,9 @@ def probe_defocus(self) -> float: else: raise RuntimeError("Condenser is not in Probe mode.") - @probe_defocus.setter - def probe_defocus(self, value: float) -> None: - if not self.__has_3cond: - raise NotImplementedError("Probe defocus exists only on 3-condenser lens systems.") - if self.condenser_mode == CondenserMode.PROBE.name: - body = RequestBody(attr=self.__id + ".ProbeDefocus", value=value) - self.__client.call(method="set", body=body) - else: - raise RuntimeError("Condenser is not in Probe mode.") - @property def convergence_angle(self) -> float: - """ Convergence angle. Works only on 3-condenser lens systems in probe mode. (read/write)""" + """ Convergence angle. Works only on 3-condenser lens systems in probe mode. """ if not self.__has_3cond: raise NotImplementedError("Probe defocus exists only on 3-condenser lens systems.") if self.condenser_mode == CondenserMode.PROBE.name: @@ -179,19 +174,18 @@ def convergence_angle(self) -> float: else: raise RuntimeError("Condenser is not in Probe mode.") - @convergence_angle.setter - def convergence_angle(self, value: float) -> None: - if not self.__has_3cond: - raise NotImplementedError("Probe defocus exists only on 3-condenser lens systems.") - if self.condenser_mode == CondenserMode.PROBE.name: - body = RequestBody(attr=self.__id + ".ConvergenceAngle", value=value) - self.__client.call(method="set", body=body) - else: - raise RuntimeError("Condenser is not in Probe mode.") - @property def C3ImageDistanceParallelOffset(self) -> float: - """ C3 image distance parallel offset. Works only on 3-condenser lens systems. (read/write)""" + """ C3 image distance parallel offset. Works only on 3-condenser lens systems. (read/write). + This value takes the place previously of the Intensity value. The Intensity value + changed the focusing of the diffraction pattern at the back-focal plane (MF-Y in Beam Settings + control panel) but was rather independent of the illumination optics. As + such it changed the size of the illumination but the illuminated area + parameter was not influenced. To get rid of this problematic bypass, + the C3 image distance offset has been created which effectively does + the same focusing but now from within the illumination optics so the + illuminated area remains correct. The range is quite small, +/-0.02 + """ if not self.__has_3cond: raise NotImplementedError("C3ImageDistanceParallelOffset exists only on 3-condenser lens systems.") if self.condenser_mode == CondenserMode.PARALLEL.name: @@ -212,7 +206,9 @@ def C3ImageDistanceParallelOffset(self, value: float) -> None: @property def mode(self) -> str: - """ Illumination mode: microprobe or nanoprobe. (read/write)""" + """ Illumination mode: microprobe or nanoprobe. (read/write) + (Nearly) no effect for low magnifications (LM). + """ body = RequestBody(attr=self.__id + ".Mode", validator=int) result = self.__client.call(method="get", body=body) diff --git a/pytemscript/modules/piezo_stage.py b/pytemscript/modules/piezo_stage.py index 7829e6b..f66e9a3 100644 --- a/pytemscript/modules/piezo_stage.py +++ b/pytemscript/modules/piezo_stage.py @@ -23,13 +23,13 @@ def __has_pstage(self) -> bool: @property def position(self) -> Dict: - """ The current position of the piezo stage (x,y,z in um). """ + """ The current position of the piezo stage (x,y,z in um and a,b in degrees). """ if not self.__has_pstage: raise NotImplementedError(self.__err_msg) else: body = RequestBody(attr=self.__id + ".CurrentPosition", validator=dict, - obj_cls=StageObj, obj_method="get") + obj_cls=StageObj, obj_method="get", a=True) return self.__client.call(method="exec_special", body=body) @property diff --git a/pytemscript/modules/projection.py b/pytemscript/modules/projection.py index d287a60..a05d23a 100644 --- a/pytemscript/modules/projection.py +++ b/pytemscript/modules/projection.py @@ -1,10 +1,10 @@ -from typing import Dict +from typing import Union, Dict, List, Tuple from collections import OrderedDict import logging from ..utils.misc import RequestBody from ..utils.enums import (ProjectionMode, ProjectionSubMode, ProjDetectorShiftMode, - ProjectionDetectorShift, LensProg, InstrumentMode) + ProjectionDetectorShift, LensProg) from .extras import Vector @@ -15,7 +15,7 @@ class Projection: def __init__(self, client): self.__client = client self.__id = "tem.Projection" - self.__err_msg = "Microscope is not in diffraction mode" + self.__err_msg = "Microscope is not in %s mode" self.__magnifications = OrderedDict() def __find_magnifications(self) -> None: @@ -72,14 +72,14 @@ def eucentric_focus(self) -> None: @property def magnification(self) -> int: - """ The reference magnification value (screen up setting).""" + """ The reference magnification value (screen up setting). (read/write)""" body = RequestBody(attr=self.__id + ".Mode", validator=int) if self.__client.call(method="get", body=body) == ProjectionMode.IMAGING: body = RequestBody(attr=self.__id + ".Magnification", validator=float) return round(self.__client.call(method="get", body=body)) else: - raise RuntimeError(self.__err_msg) + raise RuntimeError(self.__err_msg % "Imaging") @magnification.setter def magnification(self, value: int) -> None: @@ -92,7 +92,7 @@ def magnification(self, value: int) -> None: index = self.__magnifications[value][0] self.magnification_index = index else: - raise RuntimeError(self.__err_msg) + raise RuntimeError(self.__err_msg % "Imaging") @property def magnification_index(self) -> int: @@ -114,7 +114,7 @@ def camera_length(self) -> float: body = RequestBody(attr=self.__id + ".CameraLength", validator=float) return self.__client.call(method="get", body=body) else: - raise RuntimeError(self.__err_msg) + raise RuntimeError(self.__err_msg % "Diffraction") @property def camera_length_index(self) -> int: @@ -139,9 +139,9 @@ def image_shift(self) -> Vector: return Vector(x, y) * 1e6 @image_shift.setter - def image_shift(self, vector: Vector) -> None: - vector *= 1e-6 - body = RequestBody(attr=self.__id + ".ImageShift", value=vector) + def image_shift(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) * 1e-6 + body = RequestBody(attr=self.__id + ".ImageShift", value=value) self.__client.call(method="set", body=body) @property @@ -156,9 +156,9 @@ def image_beam_shift(self) -> Vector: return Vector(x, y) * 1e6 @image_beam_shift.setter - def image_beam_shift(self, vector: Vector) -> None: - vector *= 1e-6 - body = RequestBody(attr=self.__id + ".ImageBeamShift", value=vector) + def image_beam_shift(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) * 1e-6 + body = RequestBody(attr=self.__id + ".ImageBeamShift", value=value) self.__client.call(method="set", body=body) @property @@ -173,9 +173,9 @@ def image_beam_tilt(self) -> Vector: return Vector(x, y) * 1e3 @image_beam_tilt.setter - def image_beam_tilt(self, vector: Vector) -> None: - vector *= 1e-3 - body = RequestBody(attr=self.__id + ".ImageBeamTilt", value=vector) + def image_beam_tilt(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) * 1e-3 + body = RequestBody(attr=self.__id + ".ImageBeamTilt", value=value) self.__client.call(method="set", body=body) @property @@ -191,9 +191,9 @@ def diffraction_shift(self) -> Vector: return Vector(x, y) * 1e3 @diffraction_shift.setter - def diffraction_shift(self, vector: Vector) -> None: - vector *= 1e-3 - body = RequestBody(attr=self.__id + ".DiffractionShift", value=vector) + def diffraction_shift(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) * 1e-3 + body = RequestBody(attr=self.__id + ".DiffractionShift", value=value) self.__client.call(method="set", body=body) @property @@ -210,18 +210,19 @@ def diffraction_stigmator(self) -> Vector: return Vector(x, y) else: - raise RuntimeError(self.__err_msg) + raise RuntimeError(self.__err_msg % "Diffraction") @diffraction_stigmator.setter - def diffraction_stigmator(self, vector: Vector) -> None: + def diffraction_stigmator(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: body = RequestBody(attr=self.__id + ".Mode", validator=int) if self.__client.call(method="get", body=body) == ProjectionMode.DIFFRACTION: - vector.set_limits(-1.0, 1.0) - body = RequestBody(attr=self.__id + ".DiffractionStigmator", value=vector) + value = Vector.convert_to(vector) + value.set_limits(-1.0, 1.0) + body = RequestBody(attr=self.__id + ".DiffractionStigmator", value=value) self.__client.call(method="set", body=body) else: - raise RuntimeError(self.__err_msg) + raise RuntimeError(self.__err_msg % "Diffraction") @property def objective_stigmator(self) -> Vector: @@ -235,14 +236,17 @@ def objective_stigmator(self) -> Vector: return Vector(x, y) @objective_stigmator.setter - def objective_stigmator(self, vector: Vector) -> None: - vector.set_limits(-1.0, 1.0) - body = RequestBody(attr=self.__id + ".ObjectiveStigmator", value=vector) + def objective_stigmator(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) + value.set_limits(-1.0, 1.0) + body = RequestBody(attr=self.__id + ".ObjectiveStigmator", value=value) self.__client.call(method="set", body=body) @property def defocus(self) -> float: - """ Defocus value in um. (read/write)""" + """ Defocus value in um. (read/write) + Changing 'Defocus' will also change 'Focus' and vice versa. + """ body = RequestBody(attr=self.__id + ".Defocus", validator=float) return self.__client.call(method="get", body=body) * 1e6 @@ -252,6 +256,13 @@ def defocus(self, value: float) -> None: body = RequestBody(attr=self.__id + ".Defocus", value=float(value) * 1e-6) self.__client.call(method="set", body=body) + @property + def objective(self) -> float: + """ The excitation of the objective lens in percent. """ + body = RequestBody(attr=self.__id + ".ObjectiveExcitation", validator=float) + + return self.__client.call(method="get", body=body) + @property def mode(self) -> str: """ Main mode of the projection system (either imaging or diffraction). (read/write)""" diff --git a/pytemscript/modules/stem.py b/pytemscript/modules/stem.py index 42ba583..b5df976 100644 --- a/pytemscript/modules/stem.py +++ b/pytemscript/modules/stem.py @@ -1,3 +1,5 @@ +import math + from .extras import Vector from ..utils.misc import RequestBody from ..utils.enums import InstrumentMode @@ -39,7 +41,7 @@ def magnification(self) -> int: if self.__client.call(method="get", body=body) == InstrumentMode.STEM: body = RequestBody(attr="tem.Illumination.StemMagnification", validator=float) - return int(self.__client.call(method="get", body=body)) + return round(self.__client.call(method="get", body=body)) else: raise RuntimeError(self.__err_msg) @@ -55,12 +57,13 @@ def magnification(self, mag: int) -> None: @property def rotation(self) -> float: - """ The STEM rotation angle (in mrad). (read/write)""" + """ The STEM rotation angle (in degrees). (read/write)""" body = RequestBody(attr=self.__id + ".InstrumentMode", validator=int) if self.__client.call(method="get", body=body) == InstrumentMode.STEM: body = RequestBody(attr="tem.Illumination.StemRotation", validator=float) - return self.__client.call(method="get", body=body) * 1e3 + rad = self.__client.call(method="get", body=body) + return math.degrees(rad) else: raise RuntimeError(self.__err_msg) @@ -70,14 +73,14 @@ def rotation(self, rot: float) -> None: if self.__client.call(method="get", body=body) == InstrumentMode.STEM: body = RequestBody(attr="tem.Illumination.StemRotation", - value=float(rot) * 1e-3) + value=math.radians(rot)) self.__client.call(method="set", body=body) else: raise RuntimeError(self.__err_msg) @property def scan_field_of_view(self) -> Vector: - """ STEM full scan field of view. (read/write)""" + """ STEM full scan field of view in nm. """ body = RequestBody(attr=self.__id + ".InstrumentMode", validator=int) if self.__client.call(method="get", body=body) == InstrumentMode.STEM: @@ -87,16 +90,6 @@ def scan_field_of_view(self) -> Vector: x = self.__client.call(method="get", body=fov_x) y = self.__client.call(method="get", body=fov_y) - return Vector(x, y) - else: - raise RuntimeError(self.__err_msg) - - @scan_field_of_view.setter - def scan_field_of_view(self, vector: Vector) -> None: - body = RequestBody(attr=self.__id + ".InstrumentMode", validator=int) - - if self.__client.call(method="get", body=body) == InstrumentMode.STEM: - body = RequestBody(attr="tem.Illumination.StemFullScanFieldOfView", value=vector) - self.__client.call(method="set", body=body) + return Vector(x, y) * 1e9 else: raise RuntimeError(self.__err_msg) diff --git a/pytemscript/modules/vacuum.py b/pytemscript/modules/vacuum.py index b9fb11b..6cd65c4 100644 --- a/pytemscript/modules/vacuum.py +++ b/pytemscript/modules/vacuum.py @@ -75,8 +75,11 @@ def gauges(self) -> Dict: def column_open(self) -> None: """ Open column valves. """ - body = RequestBody(attr=self.__id + ".ColumnValvesOpen", value=True) - self.__client.call(method="set", body=body) + if self.status == VacuumStatus.READY.name: + body = RequestBody(attr=self.__id + ".ColumnValvesOpen", value=True) + self.__client.call(method="set", body=body) + else: + raise RuntimeError("Vacuum status is not READY") def column_close(self) -> None: """ Close column valves. """ diff --git a/pytemscript/plugins/calgetter.py b/pytemscript/plugins/calgetter.py index 5ad6746..18cbd3f 100644 --- a/pytemscript/plugins/calgetter.py +++ b/pytemscript/plugins/calgetter.py @@ -36,11 +36,7 @@ def get_magnifications(self, :param lorentz: Lorentz lens :param kv: voltage """ - result = self.cg_iface.ActualMagnifications(camera, - mode.value, - series.value, - lorentz.value, - kv) + result = self.cg_iface.ActualMagnifications(camera, mode, series, lorentz, kv) if result is not None and type(result[0]) is tuple: mag_range = {1: "LM", 2: "M", 3: "SA", 4: "Mh"} mags_dict = { @@ -95,13 +91,8 @@ def get_image_rotation(self, :param lorentz: Lorentz lens :param kv: voltage """ - return self.cg_iface.ActualTemRotation(camera, - mode.value, - magindex, - mag, - series.value, - lorentz.value, - kv) + return self.cg_iface.ActualTemRotation(camera, mode, magindex, mag, + series, lorentz, kv) def get_image_pixel_size(self, camera: str, @@ -120,13 +111,8 @@ def get_image_pixel_size(self, :param lorentz: Lorentz lens :param kv: voltage """ - res = self.cg_iface.GetPhysicalPixelSize(camera, - mode.value, - magindex, - mag, - series.value, - lorentz.value, - kv) + res = self.cg_iface.GetPhysicalPixelSize(camera, mode, magindex, mag, + series, lorentz, kv) return res[0] def basic_transform(self, @@ -154,7 +140,7 @@ def basic_transform(self, assert input_matrix.ndim == 2 x_out, y_out = self.cg_iface.BasicTransform( - transform_type.value, + transform_type, input_matrix[0, 0], input_matrix[0, 1], input_matrix[0, 2], diff --git a/pytemscript/utils/enums.py b/pytemscript/utils/enums.py index 2c396fb..c97d495 100644 --- a/pytemscript/utils/enums.py +++ b/pytemscript/utils/enums.py @@ -85,11 +85,11 @@ class StageAxes(IntEnum): class IlluminationNormalization(IntEnum): """ Normalization modes for condenser / objective lenses. """ - SPOTSIZE = 1 - INTENSITY = 2 - CONDENSER = 3 + SPOTSIZE = 1 # C1 + INTENSITY = 2 # C2+C3 + CONDENSER = 3 # C1+C2+C3 MINI_CONDENSER = 4 - OBJECTIVE = 5 + OBJECTIVE = 5 # minicondenser + objective ALL = 6 @@ -115,7 +115,7 @@ class CondenserMode(IntEnum): class ProjectionNormalization(IntEnum): """ Normalization modes for objective/projector lenses. """ OBJECTIVE = 10 - PROJECTOR = 11 + PROJECTOR = 11 # Diffraction + Intermediate + P1 + P2 ALL = 12 @@ -325,26 +325,28 @@ class LDState(IntEnum): # ---------------- FEI Tecnai CCD enums --------------------------------------- class AcqSpeed(IntEnum): - """ CCD acquisition mode. """ + """ CCD acquisition mode for TecnaiCCD plugin. """ TURBO = 0 CONTINUOUS = 1 SINGLEFRAME = 2 class AcqMode(IntEnum): - """ CCD acquisition preset.""" + """ CCD acquisition preset for TecnaiCCD plugin.""" SEARCH = 0 FOCUS = 1 RECORD = 2 # ----------------- CalGetter enums ------------------------------------------- class CalibrationStatus(IntEnum): + """ Calgetter calibratino status. """ NOT_CALIBRATED = 0 INVALID_CALIBRATION = 1 CALIBRATED = 2 class CalibrationTypes(IntEnum): + """ Calgetter calibration types. """ MAGNIFICATION = 1 BEAM_SHIFT = 2 BEAM_TILT = 3 @@ -363,6 +365,7 @@ class CalibrationTypes(IntEnum): class ModeTypes(IntEnum): + """ Illumination mode used by Calgetter. """ LM = 1 MICROPROBE = 2 NANOPROBE = 3 @@ -375,16 +378,19 @@ class ModeTypes(IntEnum): class LensSeriesTypes(IntEnum): + """ Projection mode used by Calgetter: normal (zoom) or EFTEM. """ ZOOM = 1 EFTEM = 2 class LorentzTypes(IntEnum): + """ Lorentz lens status used by Calgetter. """ OFF = 1 ON = 2 class ActualMagnificationElements(IntEnum): + """ Details of calibrated magnification from Calgetter. """ NOMINAL_MAGNIFICATION = 0 CALIBRATED_MAGNIFICATION = 1 MAGNIFICATION_INDEX = 2 @@ -402,6 +408,7 @@ class ActualMagnificationElements(IntEnum): class TransformTypes(IntEnum): + """ Calgetter transform types. """ BEAM_SHIFT_LOG = 0 BEAM_SHIFT_PHYS = 1 BEAM_TILT_LOG = 2 @@ -416,6 +423,7 @@ class TransformTypes(IntEnum): class BasicTransformTypes(IntEnum): + """ Calgetter transforms from one coordinate system to another. """ PIXEL_TO_BEAMSHIFT = 0 BEAMSHIFT_TO_PIXEL = 1 BEAMSHIFT_LOG_TO_PHYS = 2 diff --git a/pytemscript/utils/misc.py b/pytemscript/utils/misc.py index d593630..f8106e5 100644 --- a/pytemscript/utils/misc.py +++ b/pytemscript/utils/misc.py @@ -138,7 +138,11 @@ def convert_image(obj, elif use_asfile: # Save into a temp file and read into numpy - import imageio + try: + import imageio + except ImportError: + raise ImportError("imageio library not found, you cannot use " + "use_asfile kwarg.") fn = r"C:/temp.tif" if os.path.exists(fn): os.remove(fn) @@ -148,7 +152,8 @@ def convert_image(obj, else: # TecnaiCCD plugin: obj is a variant, convert to numpy - data = np.array(obj, dtype="uint16") + # Also, transpose is required to match TIA orientation + data = np.array(obj, dtype="uint16").T name = name or obj.Name diff --git a/pytemscript/utils/parse_typelib.py b/pytemscript/utils/parse_typelib.py index bba05eb..4c7dc69 100644 --- a/pytemscript/utils/parse_typelib.py +++ b/pytemscript/utils/parse_typelib.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import comtypes.client -from pytemscript.utils.constants import SCRIPTING_STD, SCRIPTING_ADV +from pytemscript.utils.constants import * EXCLUDED_METHODS = [ "QueryInterface", @@ -60,7 +60,14 @@ def list_typelib_details(prog_id: str): def create_output(): """ Save output into txt. """ - for prog_id in [SCRIPTING_STD, SCRIPTING_ADV]: + for prog_id in [ + SCRIPTING_STD, + SCRIPTING_ADV, + SCRIPTING_LOWDOSE, + SCRIPTING_TIA, + SCRIPTING_TECNAI_CCD2, + SCRIPTING_TECNAI_CCD + ]: print("Querying %s..." % prog_id, end="") enums, interfaces, version = list_typelib_details(prog_id) if enums is not None: diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 7044174..4a5632a 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 AcqImageSize +from pytemscript.utils.enums import AcqImageSize, ScreenPosition from pytemscript.modules.extras import Image @@ -34,8 +34,8 @@ def print_stats(image: Image, assert int(metadata['Binning.Width']) == binning assert isclose(float(metadata['ExposureTime']), exp_time, abs_tol=0.05) - assert img.shape[0] == metadata["width"] - assert img.shape[1] == metadata["height"] + assert img.shape[1] == metadata["width"] + assert img.shape[0] == metadata["height"] if interactive: import matplotlib.pyplot as plt @@ -131,8 +131,8 @@ def main(argv: Optional[List] = None) -> None: 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}, + "BM-Ceta": {"exp_time": 1.0, "binning": 1}, + "BM-Falcon": {"exp_time": 0.5, "binning": 1}, "EF-CCD": {"exp_time": 2.0, "binning": 1}, } acq_csa_params = { @@ -142,15 +142,18 @@ def main(argv: Optional[List] = None) -> None: "electron_counting": True, "save_frames": True, "group_frames": 2}, } - for cam, cam_dict in cameras.items(): + def check_mode(): 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() + microscope.acquisition.screen_position = ScreenPosition.UP + for cam, cam_dict in cameras.items(): csa = cam_dict["supports_csa"] if csa and cam in acq_csa_params: csa_params = acq_csa_params[cam] + check_mode() camera_acquire(microscope, cam, **csa_params) if cam_dict["supports_eer"]: @@ -159,13 +162,15 @@ def main(argv: Optional[List] = None) -> None: camera_acquire(microscope, cam, **csa_params) elif cam in acq_params: + check_mode() camera_acquire(microscope, cam, **acq_params[cam]) if microscope.stem.is_available: microscope.stem.enable() + microscope.stem.magnification = 28000 detectors = microscope.acquisition.stem_detectors for d in detectors: - detector_acquire(microscope, d, dwell_time=1e-6, binning=2) + detector_acquire(microscope, d, dwell_time=5e-6, binning=1) microscope.stem.disable() diff --git a/tests/test_events.py b/tests/test_events.py index b62ce69..a77b3e3 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -14,7 +14,7 @@ def main() -> None: print("User buttons:", buttons.show()) def screen_toggle(): - current_pos = acquisition.screen + current_pos = acquisition.screen_position if current_pos == ScreenPosition.UP.name: new_pos = ScreenPosition.DOWN elif current_pos == ScreenPosition.DOWN.name: @@ -22,9 +22,8 @@ def screen_toggle(): else: raise RuntimeError("Unknown screen position: %s" % current_pos) - acquisition.screen = new_pos + acquisition.screen_position = new_pos print("New screen position:", new_pos) - assert new_pos.name == acquisition.screen event_handler = ButtonHandler(buttons.L1, lambda: screen_toggle(), diff --git a/tests/test_microscope.py b/tests/test_microscope.py index 4d7a1aa..3a10436 100644 --- a/tests/test_microscope.py +++ b/tests/test_microscope.py @@ -10,7 +10,6 @@ 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.modules import Vector from pytemscript.utils.enums import * @@ -26,35 +25,56 @@ def test_projection(microscope: Microscope, print("\tFocus:", projection.focus) print("\tDefocus:", projection.defocus) - orig_def = projection.defocus projection.defocus = -3.0 assert isclose(projection.defocus, -3.0, abs_tol=1e-5) - projection.defocus = orig_def + projection.focus = 0.1 + assert isclose(projection.focus, 0.1, abs_tol=1e-5) + projection.eucentric_focus() + print("\tObjective:", projection.objective) print("\tMagnification:", projection.magnification) print("\tMagnificationIndex:", projection.magnification_index) + projection.magnification_index += 1 + projection.magnification_index -= 1 projection.mode = ProjectionMode.DIFFRACTION print("\tCameraLength:", projection.camera_length) print("\tCameraLengthIndex:", projection.camera_length_index) + projection.camera_length_index += 1 + projection.camera_length_index -= 1 + print("\tDiffractionShift:", projection.diffraction_shift) + projection.diffraction_shift += (-0.02, 0.02) + projection.diffraction_shift -= (-0.02, 0.02) + print("\tDiffractionStigmator:", projection.diffraction_stigmator) + projection.diffraction_stigmator += (-0.02, 0.02) + projection.diffraction_stigmator -= (-0.02, 0.02) projection.mode = ProjectionMode.IMAGING print("\tImageShift:", projection.image_shift) - projection.image_shift = Vector(-0,0) + projection.image_shift = (0,0) print("\tImageBeamShift:", projection.image_beam_shift) + projection.image_beam_shift = [0,0] print("\tObjectiveStigmator:", projection.objective_stigmator) + projection.objective_stigmator += (-0.02, 0.02) + projection.objective_stigmator -= (-0.02, 0.02) + print("\tSubMode:", projection.magnification_range) print("\tLensProgram:", projection.is_eftem_on) print("\tImageRotation:", projection.image_rotation) print("\tDetectorShift:", projection.detector_shift) print("\tDetectorShiftMode:", projection.detector_shift_mode) - print("\tImageBeamTilt:", projection.image_beam_tilt) - print("\tLensProgram:", projection.is_eftem_on) - projection.reset_defocus() # TODO: not working remotely? check _exec! + beam_tilt = projection.image_beam_tilt + print("\tImageBeamTilt:", beam_tilt) + projection.image_beam_tilt = [-0.02, 0.03] + projection.image_beam_tilt = beam_tilt + + print("\tIsEftemOn:", projection.is_eftem_on) + + projection.reset_defocus() # TODO: not working remotely? if has_eftem: print("\tToggling EFTEM mode...") @@ -73,6 +93,7 @@ def test_acquisition(microscope: Microscope) -> None: print("\tFilm settings:", acquisition.film_settings) print("\tCameras:", cameras) + acquisition.screen_position = ScreenPosition.UP for cam_name in cameras: image = acquisition.acquire_tem_image(cam_name, @@ -145,6 +166,12 @@ def test_temperature(microscope: Microscope, except Exception as e: print(str(e)) + if microscope.family == ProductFamily.TITAN.name: + print("\tDocker temperature:", temp.temp_docker) + print("\tCassette temperature:", temp.temp_cassette) + print("\tCartridge temperature:", temp.temp_cartridge) + print("\tHolder temperature:", temp.temp_holder) + def test_autoloader(microscope: Microscope, check_loading: bool = False, @@ -163,6 +190,10 @@ def test_autoloader(microscope: Microscope, if check_loading: try: print("\tRunning inventory and trying to load cartridge #%d..." % slot) + al.initialize() + al.buffer_cycle() + #al.undock_cassette() + #al.dock_cassette() al.run_inventory() if al.slot_status(slot) == CassetteSlotStatus.OCCUPIED.name: al.load_cartridge(slot) @@ -189,8 +220,8 @@ def test_stage(microscope: Microscope) -> None: stage.go_to(x=1, y=-1) sleep(1) print("\tPosition:", stage.position) - print("\tGoto(x=-1, speed=0.5)") - stage.go_to(x=-1, speed=0.5) + print("\tGoto(x=-1, speed=0.25)") + stage.go_to(x=-1, speed=0.25) sleep(1) print("\tPosition:", stage.position) print("\tMoveTo() to original position") @@ -204,6 +235,7 @@ def test_optics(microscope: Microscope) -> None: """ print("\nTesting optics...") opt = microscope.optics + print("\tInstrumentMode:", opt.instrument_mode) print("\tScreenCurrent:", opt.screen_current) print("\tBeamBlanked:", opt.is_beam_blanked) print("\tAutoNormalizeEnabled:", opt.is_autonormalize_on) @@ -221,12 +253,11 @@ def test_illumination(microscope: Microscope) -> None: print("\nTesting illumination...") illum = microscope.optics.illumination print("\tMode:", illum.mode) + illum.mode = IlluminationMode.NANOPROBE print("\tSpotsizeIndex:", illum.spotsize) - orig_spot = illum.spotsize - illum.spotsize = 5 - assert illum.spotsize == 5 - illum.spotsize = orig_spot + illum.spotsize += 1 + illum.spotsize -= 1 if microscope.condenser_system == CondenserLensSystem.TWO_CONDENSER_LENSES.name: print("\tIntensity:", illum.intensity) @@ -237,11 +268,14 @@ def test_illumination(microscope: Microscope) -> None: illum.intensity = orig_int print("\tIntensityZoomEnabled:", illum.intensity_zoom) + illum.intensity_zoom = False print("\tIntensityLimitEnabled:", illum.intensity_limit) + illum.intensity_limit = False elif microscope.condenser_system == CondenserLensSystem.THREE_CONDENSER_LENSES.name: print("\tCondenserMode:", illum.condenser_mode) print("\tIntensityZoomEnabled:", illum.intensity_zoom) + illum.intensity_zoom = False print("\tIlluminatedArea:", illum.illuminated_area) illum.condenser_mode = CondenserMode.PROBE @@ -250,6 +284,8 @@ def test_illumination(microscope: Microscope) -> None: illum.condenser_mode = CondenserMode.PARALLEL print("\tC3ImageDistanceParallelOffset:", illum.C3ImageDistanceParallelOffset) + illum.C3ImageDistanceParallelOffset += 0.01 + illum.C3ImageDistanceParallelOffset -= 0.01 orig_illum = illum.illuminated_area illum.illuminated_area = 1.0 @@ -258,15 +294,22 @@ def test_illumination(microscope: Microscope) -> None: print("\tShift:", illum.beam_shift) - illum.beam_shift = Vector(0.5, 0.5) - illum.beam_shift = Vector(0, 0) + illum.beam_shift = (0.5, 0.5) + illum.beam_shift = [0, 0] print("\tCondenserStigmator:", illum.condenser_stigmator) print("\tRotationCenter:", illum.rotation_center) + illum.rotation_center += (0.1, 0.2) + illum.rotation_center -= (0.1, 0.2) if microscope.family != ProductFamily.TECNAI.name: print("\tTilt:", illum.beam_tilt) print("\tDFMode:", illum.dark_field) + illum.dark_field = DarkFieldMode.CARTESIAN + print("\tTilt (cartesian):", illum.beam_tilt) + illum.dark_field = DarkFieldMode.CONICAL + print("\tTilt (conical):", illum.beam_tilt) + illum.dark_field = DarkFieldMode.OFF def test_stem(microscope: Microscope) -> None: @@ -280,7 +323,9 @@ def test_stem(microscope: Microscope) -> None: if stem.is_available: stem.enable() print("\tIllumination.StemMagnification:", stem.magnification) + stem.magnification = 28000 print("\tIllumination.StemRotation:", stem.rotation) + stem.rotation = -89.0 print("\tIllumination.StemFullScanFieldOfView:", stem.scan_field_of_view) stem.disable() @@ -296,10 +341,17 @@ def test_gun(microscope: Microscope, print("\tHTValue:", gun.voltage) print("\tHTMaxValue:", gun.voltage_max) print("\tShift:", gun.shift) + gun.shift += (0.01, 0.02) + gun.shift -= (0.01, 0.02) + print("\tTilt:", gun.tilt) + gun.tilt += (0.01, 0.02) + gun.tilt -= (0.01, 0.02) try: print("\tHVOffset:", gun.voltage_offset) + gun.voltage_offset += 0.1 + gun.voltage_offset -= 0.1 print("\tHVOffsetRange:", gun.voltage_offset_range) except NotImplementedError: pass @@ -308,9 +360,10 @@ def test_gun(microscope: Microscope, print("\tFegState:", gun.feg_state) print("\tHTState:", gun.ht_state) print("\tBeamCurrent:", gun.beam_current) - print("\tFocusIndex:", gun.focus_index) + print("\tGunLens:", gun.gun_lens) try: + gun.is_flashing_advised(FegFlashingType.HIGH_T) gun.do_flashing(FegFlashingType.LOW_T) gun.do_flashing(FegFlashingType.HIGH_T) except Warning: @@ -334,9 +387,9 @@ def test_apertures(microscope: Microscope, if has_license: aps.show() - aps.disable("C2") - aps.enable("C2") - aps.select("C2", 50) + aps.enable(MechanismId.C2) + aps.select(MechanismId.C2, 50) + aps.retract(MechanismId.OBJ) def test_energy_filter(microscope: Microscope) -> None: @@ -349,10 +402,16 @@ def test_energy_filter(microscope: Microscope) -> None: ef = microscope.energy_filter print("\tZLPShift: ", ef.zlp_shift) + ef.zlp_shift += 10 + ef.zlp_shift -= 10 + print("\tHTShift: ", ef.ht_shift) + ef.ht_shift += 10 + ef.ht_shift -= 10 ef.insert_slit(10) print("\tSlit width: ", ef.slit_width) + ef.slit_width = 20 ef.retract_slit() except: pass @@ -378,8 +437,6 @@ def test_general(microscope: Microscope, """ print("\nTesting configuration...") print("\tConfiguration.ProductFamily:", microscope.family) - print("\tBlankerShutter.ShutterOverrideOn:", - microscope.optics.is_shutter_override_on) print("\tCondenser system:", microscope.condenser_system) if microscope.family == ProductFamily.TITAN.name: diff --git a/tests/test_speed.py b/tests/test_speed.py index daf6f3c..596fa48 100644 --- a/tests/test_speed.py +++ b/tests/test_speed.py @@ -14,12 +14,12 @@ def acquire_image(microscope: Microscope, camera: str, **kwargs) -> Image: def main() -> None: """ Testing acquisition speed. """ - microscope = Microscope(debug=True, useTecnaiCCD=False) + microscope = Microscope(debug=True, useTecnaiCCD=True) print("Starting acquisition speed test") cameras = microscope.acquisition.cameras - for camera in ["BM-Ceta", "BM-Falcon", "EF-Falcon", "EF-CCD"]: + for camera in ["BM-Orius", "BM-Ceta", "BM-Falcon", "EF-Falcon", "EF-CCD"]: if camera in cameras: print("\tUsing SafeArray") @@ -32,7 +32,7 @@ def main() -> None: img2.save(r"C:/%s_asfile.mrc" % camera, overwrite=True) if camera in ["EF-CCD", "BM-Orius"]: - print("\tUsing TecnaiCCD/TIA") + print("\tUsing TecnaiCCD") # 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)