From bd90e8dad5ace0b37c639f1b5c50174de852eea1 Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Thu, 20 Mar 2025 17:10:17 +0000 Subject: [PATCH 01/12] trying new publish approach --- .github/workflows/publish_and_tag.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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") From 2eba782addaa7368633dcb678696e391aef13e7c Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Thu, 20 Mar 2025 19:47:36 +0000 Subject: [PATCH 02/12] add r/w notes, add is_flashing_advised method --- pytemscript/__init__.py | 2 +- pytemscript/modules/energyfilter.py | 6 +++--- pytemscript/modules/gun.py | 17 ++++++++++++++--- pytemscript/modules/projection.py | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) 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/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/gun.py b/pytemscript/modules/gun.py index 06a8dd4..93448a9 100644 --- a/pytemscript/modules/gun.py +++ b/pytemscript/modules/gun.py @@ -242,12 +242,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/projection.py b/pytemscript/modules/projection.py index d287a60..9b48922 100644 --- a/pytemscript/modules/projection.py +++ b/pytemscript/modules/projection.py @@ -72,7 +72,7 @@ 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: From 35e7d1e96a610160347368d22268f962ba30dab8 Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Fri, 21 Mar 2025 13:29:38 +0000 Subject: [PATCH 03/12] improve docs and docstrings, add objective lens excitation, rename focus_index to gun_lens, imageio is optional --- README.rst | 2 ++ docs/changelog.rst | 5 ----- docs/components/events.rst | 2 +- docs/conf.py | 2 +- docs/index.rst | 2 ++ docs/installation.rst | 10 ++++++++- docs/remote.rst | 7 +++--- pytemscript/microscope.py | 5 ++--- pytemscript/modules/acquisition.py | 34 +++++++++++++++++++++-------- pytemscript/modules/extras.py | 14 ++++-------- pytemscript/modules/gun.py | 4 ++-- pytemscript/modules/illumination.py | 27 ++++++++++++++++++----- pytemscript/modules/projection.py | 25 ++++++++++++++------- pytemscript/utils/enums.py | 22 +++++++++++++------ pytemscript/utils/misc.py | 6 ++++- tests/test_microscope.py | 1 + 16 files changed, 111 insertions(+), 57 deletions(-) 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/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/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..7df79ae 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:: diff --git a/docs/installation.rst b/docs/installation.rst index 39653c7..b288e3f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,13 +1,21 @@ 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) + +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/microscope.py b/pytemscript/microscope.py index c94dc8f..f52df0f 100644 --- a/pytemscript/microscope.py +++ b/pytemscript/microscope.py @@ -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..b0d2af0 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]: @@ -187,7 +187,7 @@ def set_tem_presets(self, prev_shutter_mode = None if 'correction' in kwargs: - settings.ImageCorrection = kwargs['correction'] + settings.ImageCorrection = kwargs.get('correction', AcqImageCorrection.DEFAULT) if 'exposure_mode' in kwargs: settings.ExposureMode = kwargs['exposure_mode'] if 'shutter_mode' in kwargs: @@ -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: @@ -464,7 +464,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 +476,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 +490,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 @@ -581,7 +594,7 @@ 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 @@ -637,7 +650,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: @@ -676,7 +689,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 +702,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/extras.py b/pytemscript/modules/extras.py index ea6f640..d1692d3 100644 --- a/pytemscript/modules/extras.py +++ b/pytemscript/modules/extras.py @@ -6,14 +6,8 @@ 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 @@ -125,7 +119,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,7 +187,7 @@ 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. diff --git a/pytemscript/modules/gun.py b/pytemscript/modules/gun.py index 93448a9..ded8798 100644 --- a/pytemscript/modules/gun.py +++ b/pytemscript/modules/gun.py @@ -223,8 +223,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) diff --git a/pytemscript/modules/illumination.py b/pytemscript/modules/illumination.py index ea33922..86110cc 100644 --- a/pytemscript/modules/illumination.py +++ b/pytemscript/modules/illumination.py @@ -133,19 +133,23 @@ def condenser_stigmator(self, vector: Vector) -> None: @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: @@ -191,7 +195,16 @@ def convergence_angle(self, value: float) -> None: @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. + """ if not self.__has_3cond: raise NotImplementedError("C3ImageDistanceParallelOffset exists only on 3-condenser lens systems.") if self.condenser_mode == CondenserMode.PARALLEL.name: @@ -212,7 +225,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/projection.py b/pytemscript/modules/projection.py index 9b48922..aeb6184 100644 --- a/pytemscript/modules/projection.py +++ b/pytemscript/modules/projection.py @@ -4,7 +4,7 @@ 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: @@ -79,7 +79,7 @@ def magnification(self) -> int: 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: @@ -210,7 +210,7 @@ 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: @@ -221,7 +221,7 @@ def diffraction_stigmator(self, vector: Vector) -> None: body = RequestBody(attr=self.__id + ".DiffractionStigmator", value=vector) 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: @@ -242,7 +242,9 @@ def objective_stigmator(self, vector: Vector) -> None: @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 +254,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/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..0f6a5f1 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) diff --git a/tests/test_microscope.py b/tests/test_microscope.py index 4d7a1aa..165dbc4 100644 --- a/tests/test_microscope.py +++ b/tests/test_microscope.py @@ -31,6 +31,7 @@ def test_projection(microscope: Microscope, assert isclose(projection.defocus, -3.0, abs_tol=1e-5) projection.defocus = orig_def + print("\tObjective:", projection.objective) print("\tMagnification:", projection.magnification) print("\tMagnificationIndex:", projection.magnification_index) From ff30e2c73ac22f4cd74ee9b716c9d6b950290235 Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Fri, 21 Mar 2025 14:45:51 +0000 Subject: [PATCH 04/12] for vector attrs allow to set from a list or tuple --- pytemscript/modules/extras.py | 24 ++++++++++++----- pytemscript/modules/gun.py | 16 +++++++----- pytemscript/modules/illumination.py | 21 +++++++-------- pytemscript/modules/projection.py | 40 +++++++++++++++-------------- pytemscript/modules/stem.py | 7 +++-- pytemscript/utils/parse_typelib.py | 11 ++++++-- 6 files changed, 73 insertions(+), 46 deletions(-) diff --git a/pytemscript/modules/extras.py b/pytemscript/modules/extras.py index d1692d3..528bcb2 100644 --- a/pytemscript/modules/extras.py +++ b/pytemscript/modules/extras.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, Tuple, Union +from typing import Optional, Dict, Tuple, Union, List from datetime import datetime import math import logging @@ -45,6 +45,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 @@ -67,12 +77,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): diff --git a/pytemscript/modules/gun.py b/pytemscript/modules/gun.py index ded8798..f4eb676 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 diff --git a/pytemscript/modules/illumination.py b/pytemscript/modules/illumination.py index 86110cc..366c735 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,9 +125,10 @@ 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 diff --git a/pytemscript/modules/projection.py b/pytemscript/modules/projection.py index aeb6184..0626d5a 100644 --- a/pytemscript/modules/projection.py +++ b/pytemscript/modules/projection.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Union, Dict, List, Tuple from collections import OrderedDict import logging @@ -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 @@ -213,12 +213,13 @@ def diffraction_stigmator(self) -> Vector: 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 % "Diffraction") @@ -235,9 +236,10 @@ 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 diff --git a/pytemscript/modules/stem.py b/pytemscript/modules/stem.py index 42ba583..b46b33a 100644 --- a/pytemscript/modules/stem.py +++ b/pytemscript/modules/stem.py @@ -1,3 +1,5 @@ +from typing import Union, List, Tuple + from .extras import Vector from ..utils.misc import RequestBody from ..utils.enums import InstrumentMode @@ -92,11 +94,12 @@ def scan_field_of_view(self) -> Vector: raise RuntimeError(self.__err_msg) @scan_field_of_view.setter - def scan_field_of_view(self, vector: Vector) -> None: + def scan_field_of_view(self, vector: Union[Vector, List[float], Tuple[float, float]]) -> None: + value = Vector.convert_to(vector) 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) + body = RequestBody(attr="tem.Illumination.StemFullScanFieldOfView", value=value) self.__client.call(method="set", body=body) else: raise RuntimeError(self.__err_msg) 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: From 4f8b2cb150c27e0ec268c5519fbae894fd80d6ee Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Mon, 24 Mar 2025 10:26:19 +0000 Subject: [PATCH 05/12] transpose array from tecnaiccd plugin to match TIA --- pytemscript/utils/misc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytemscript/utils/misc.py b/pytemscript/utils/misc.py index 0f6a5f1..f8106e5 100644 --- a/pytemscript/utils/misc.py +++ b/pytemscript/utils/misc.py @@ -152,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 From 4a7d22215771806279aab53127564257d67062e6 Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Mon, 24 Mar 2025 11:00:21 +0000 Subject: [PATCH 06/12] add a doc about acquisition --- docs/acquisition.rst | 99 ++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + tests/test_speed.py | 6 +-- 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 docs/acquisition.rst diff --git a/docs/acquisition.rst b/docs/acquisition.rst new file mode 100644 index 0000000..4430c2c --- /dev/null +++ b/docs/acquisition.rst @@ -0,0 +1,99 @@ +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. + +Standard scripting is also used by all STEM detectors. 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) diff --git a/docs/index.rst b/docs/index.rst index 7df79ae..6c4922a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,6 +29,7 @@ The documentation can be found at https://pytemscript.readthedocs.io installation components/index getting_started + acquisition remote changelog 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) From 6dab298435eb0b4ced11393ebf4affdb09fbc709 Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Mon, 24 Mar 2025 11:23:08 +0000 Subject: [PATCH 07/12] set default camera size for stem, test vector setting from a tuple or list --- docs/components/index.rst | 5 ++++- docs/installation.rst | 1 + pytemscript/modules/acquisition.py | 2 +- pytemscript/modules/projection.py | 2 +- tests/test_microscope.py | 7 +++---- 5 files changed, 10 insertions(+), 7 deletions(-) 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/installation.rst b/docs/installation.rst index b288e3f..d54f379 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -7,6 +7,7 @@ Prerequisites for the FEI or Thermo Fisher Scientific microscope: * TEM Advanced scripting (optional) * LowDose (optional) * TecnaiCCD plugin for Digital Micrograph (optional) + * SerialEMCCD plugin for Digital Micrograph (optional) Requirements for this package: diff --git a/pytemscript/modules/acquisition.py b/pytemscript/modules/acquisition.py index b0d2af0..41bff85 100644 --- a/pytemscript/modules/acquisition.py +++ b/pytemscript/modules/acquisition.py @@ -582,7 +582,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: diff --git a/pytemscript/modules/projection.py b/pytemscript/modules/projection.py index 0626d5a..a05d23a 100644 --- a/pytemscript/modules/projection.py +++ b/pytemscript/modules/projection.py @@ -245,7 +245,7 @@ def objective_stigmator(self, vector: Union[Vector, List[float], Tuple[float, fl @property def defocus(self) -> float: """ Defocus value in um. (read/write) - Changing ‘Defocus’ will also change ‘Focus’ and vice versa. + Changing 'Defocus' will also change 'Focus' and vice versa. """ body = RequestBody(attr=self.__id + ".Defocus", validator=float) diff --git a/tests/test_microscope.py b/tests/test_microscope.py index 165dbc4..27f7659 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 * @@ -43,7 +42,7 @@ def test_projection(microscope: Microscope, 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) print("\tObjectiveStigmator:", projection.objective_stigmator) @@ -259,8 +258,8 @@ 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) From 1e3f947c0855c160d715dec3f2a83e3c2b74e4de Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Mon, 24 Mar 2025 16:32:08 +0000 Subject: [PATCH 08/12] 1) stem rotation in degrees, 2) stem fov is read only, 3) fix img shape assert, 4) other minor changes --- docs/acquisition.rst | 16 ++++++++++++---- pytemscript/modules/acquisition.py | 15 +++------------ pytemscript/modules/stem.py | 24 +++++++----------------- tests/test_acquisition.py | 13 ++++++++----- 4 files changed, 30 insertions(+), 38 deletions(-) diff --git a/docs/acquisition.rst b/docs/acquisition.rst index 4430c2c..c38fe22 100644 --- a/docs/acquisition.rst +++ b/docs/acquisition.rst @@ -14,8 +14,8 @@ List of tested cameras: * 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. +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 ------------------ @@ -31,8 +31,8 @@ and TIA to be opened as well as the current camera selected in the Microscope Us .. 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. -Standard scripting is also used by all STEM detectors. 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). +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 @@ -97,3 +97,11 @@ If you want to try this method, add a couple of kwargs to your acquisition comma 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/pytemscript/modules/acquisition.py b/pytemscript/modules/acquisition.py index 41bff85..193b0d4 100644 --- a/pytemscript/modules/acquisition.py +++ b/pytemscript/modules/acquisition.py @@ -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): @@ -596,8 +587,8 @@ def acquire_stem_image(self, :type dwell_time: float :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) diff --git a/pytemscript/modules/stem.py b/pytemscript/modules/stem.py index b46b33a..bcecd67 100644 --- a/pytemscript/modules/stem.py +++ b/pytemscript/modules/stem.py @@ -1,4 +1,4 @@ -from typing import Union, List, Tuple +import math from .extras import Vector from ..utils.misc import RequestBody @@ -57,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) @@ -72,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(float(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: @@ -89,17 +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: Union[Vector, List[float], Tuple[float, float]]) -> None: - value = Vector.convert_to(vector) - 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=value) - self.__client.call(method="set", body=body) + return Vector(x, y) * 1e9 else: raise RuntimeError(self.__err_msg) diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 7044174..6ac280b 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -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,17 @@ 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() + 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,6 +161,7 @@ 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: From 1f3970e2cb81edab47d19b8f6900743ae80ca6d1 Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Tue, 25 Mar 2025 12:43:47 +0000 Subject: [PATCH 09/12] use enums for apertures, enhance main test --- pytemscript/modules/acquisition.py | 4 +- pytemscript/modules/apertures.py | 42 ++++++------- pytemscript/modules/stem.py | 2 +- pytemscript/plugins/calgetter.py | 26 ++------ tests/test_microscope.py | 97 ++++++++++++++++++++++++------ 5 files changed, 109 insertions(+), 62 deletions(-) diff --git a/pytemscript/modules/acquisition.py b/pytemscript/modules/acquisition.py index 193b0d4..d9885fa 100644 --- a/pytemscript/modules/acquisition.py +++ b/pytemscript/modules/acquisition.py @@ -631,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: 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/stem.py b/pytemscript/modules/stem.py index bcecd67..a4180cc 100644 --- a/pytemscript/modules/stem.py +++ b/pytemscript/modules/stem.py @@ -73,7 +73,7 @@ def rotation(self, rot: float) -> None: if self.__client.call(method="get", body=body) == InstrumentMode.STEM: body = RequestBody(attr="tem.Illumination.StemRotation", - value=math.radians(float(rot))) + value=math.radians(rot)) self.__client.call(method="set", body=body) else: raise RuntimeError(self.__err_msg) 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/tests/test_microscope.py b/tests/test_microscope.py index 27f7659..96145a7 100644 --- a/tests/test_microscope.py +++ b/tests/test_microscope.py @@ -25,36 +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 *= 2 + projection.diffraction_stigmator /= 2 projection.mode = ProjectionMode.IMAGING print("\tImageShift:", projection.image_shift) - projection.image_shift = (-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 *= 2 + projection.objective_stigmator /= 2 + 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 = 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,19 +268,28 @@ 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 print("\tProbeDefocus:", illum.probe_defocus) + illum.probe_defocus += 0.1 + illum.probe_defocus -= 0.1 print("\tConvergenceAngle:", illum.convergence_angle) + illum.convergence_angle += 0.1 + illum.convergence_angle -= 0.1 illum.condenser_mode = CondenserMode.PARALLEL print("\tC3ImageDistanceParallelOffset:", illum.C3ImageDistanceParallelOffset) + illum.C3ImageDistanceParallelOffset += 0.1 + illum.C3ImageDistanceParallelOffset -= 0.1 orig_illum = illum.illuminated_area illum.illuminated_area = 1.0 @@ -263,10 +303,17 @@ def test_illumination(microscope: Microscope) -> None: 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 +327,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 +345,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 +364,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 +391,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 +406,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 +441,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: From 5f9546b5c4949fb6f4d9484c46cd8c9ab9b1c60b Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Wed, 26 Mar 2025 09:01:30 +0000 Subject: [PATCH 10/12] minor corrections --- pytemscript/modules/acquisition.py | 2 +- pytemscript/modules/gun.py | 2 +- pytemscript/modules/stem.py | 2 +- tests/test_acquisition.py | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pytemscript/modules/acquisition.py b/pytemscript/modules/acquisition.py index d9885fa..b74e80c 100644 --- a/pytemscript/modules/acquisition.py +++ b/pytemscript/modules/acquisition.py @@ -187,7 +187,7 @@ def set_tem_presets(self, prev_shutter_mode = None if 'correction' in kwargs: - settings.ImageCorrection = kwargs.get('correction', AcqImageCorrection.DEFAULT) + settings.ImageCorrection = kwargs['correction'] if 'exposure_mode' in kwargs: settings.ExposureMode = kwargs['exposure_mode'] if 'shutter_mode' in kwargs: diff --git a/pytemscript/modules/gun.py b/pytemscript/modules/gun.py index f4eb676..d56f88a 100644 --- a/pytemscript/modules/gun.py +++ b/pytemscript/modules/gun.py @@ -174,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) diff --git a/pytemscript/modules/stem.py b/pytemscript/modules/stem.py index a4180cc..b5df976 100644 --- a/pytemscript/modules/stem.py +++ b/pytemscript/modules/stem.py @@ -41,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) diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 6ac280b..b9f3bd6 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -166,9 +166,10 @@ def check_mode(): 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() From 03ac7e5209d80cf5ef3e8bd54e7b0b5724528894 Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Wed, 26 Mar 2025 09:51:43 +0000 Subject: [PATCH 11/12] probe defocus and conv angle are read-only --- pytemscript/modules/illumination.py | 24 ++---------------------- tests/test_microscope.py | 8 ++------ 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/pytemscript/modules/illumination.py b/pytemscript/modules/illumination.py index 366c735..0905ae3 100644 --- a/pytemscript/modules/illumination.py +++ b/pytemscript/modules/illumination.py @@ -154,7 +154,7 @@ def illuminated_area(self, value: float) -> None: @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: @@ -163,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: @@ -184,16 +174,6 @@ 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). diff --git a/tests/test_microscope.py b/tests/test_microscope.py index 96145a7..be82399 100644 --- a/tests/test_microscope.py +++ b/tests/test_microscope.py @@ -280,16 +280,12 @@ def test_illumination(microscope: Microscope) -> None: illum.condenser_mode = CondenserMode.PROBE print("\tProbeDefocus:", illum.probe_defocus) - illum.probe_defocus += 0.1 - illum.probe_defocus -= 0.1 print("\tConvergenceAngle:", illum.convergence_angle) - illum.convergence_angle += 0.1 - illum.convergence_angle -= 0.1 illum.condenser_mode = CondenserMode.PARALLEL print("\tC3ImageDistanceParallelOffset:", illum.C3ImageDistanceParallelOffset) - illum.C3ImageDistanceParallelOffset += 0.1 - illum.C3ImageDistanceParallelOffset -= 0.1 + illum.C3ImageDistanceParallelOffset += 0.01 + illum.C3ImageDistanceParallelOffset -= 0.01 orig_illum = illum.illuminated_area illum.illuminated_area = 1.0 From 98f2927669c32ef1dbe98aa346dc456ad4649ee8 Mon Sep 17 00:00:00 2001 From: Grigory Sharov Date: Wed, 26 Mar 2025 12:45:01 +0000 Subject: [PATCH 12/12] fixes for 3.4 after test on spirit, rename screen to screen_position --- pytemscript/microscope.py | 2 +- pytemscript/modules/acquisition.py | 6 +++--- pytemscript/modules/extras.py | 13 ++++++------- pytemscript/modules/illumination.py | 2 +- pytemscript/modules/piezo_stage.py | 4 ++-- pytemscript/modules/vacuum.py | 7 +++++-- tests/test_acquisition.py | 3 ++- tests/test_events.py | 5 ++--- tests/test_microscope.py | 10 +++++----- 9 files changed, 27 insertions(+), 25 deletions(-) diff --git a/pytemscript/microscope.py b/pytemscript/microscope.py index f52df0f..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: diff --git a/pytemscript/modules/acquisition.py b/pytemscript/modules/acquisition.py index b74e80c..9836d2d 100644 --- a/pytemscript/modules/acquisition.py +++ b/pytemscript/modules/acquisition.py @@ -655,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) diff --git a/pytemscript/modules/extras.py b/pytemscript/modules/extras.py index 528bcb2..bd0420c 100644 --- a/pytemscript/modules/extras.py +++ b/pytemscript/modules/extras.py @@ -2,6 +2,7 @@ from datetime import datetime import math import logging +import os.path from pathlib import Path import numpy as np from functools import lru_cache @@ -206,10 +207,8 @@ def save(self, :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 @@ -219,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') @@ -230,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/illumination.py b/pytemscript/modules/illumination.py index 0905ae3..69e8012 100644 --- a/pytemscript/modules/illumination.py +++ b/pytemscript/modules/illumination.py @@ -184,7 +184,7 @@ def C3ImageDistanceParallelOffset(self) -> float: 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. + 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.") 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/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/tests/test_acquisition.py b/tests/test_acquisition.py index b9f3bd6..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 @@ -148,6 +148,7 @@ def check_mode(): 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: 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 be82399..3a10436 100644 --- a/tests/test_microscope.py +++ b/tests/test_microscope.py @@ -48,8 +48,8 @@ def test_projection(microscope: Microscope, projection.diffraction_shift -= (-0.02, 0.02) print("\tDiffractionStigmator:", projection.diffraction_stigmator) - projection.diffraction_stigmator *= 2 - projection.diffraction_stigmator /= 2 + projection.diffraction_stigmator += (-0.02, 0.02) + projection.diffraction_stigmator -= (-0.02, 0.02) projection.mode = ProjectionMode.IMAGING print("\tImageShift:", projection.image_shift) @@ -58,8 +58,8 @@ def test_projection(microscope: Microscope, print("\tImageBeamShift:", projection.image_beam_shift) projection.image_beam_shift = [0,0] print("\tObjectiveStigmator:", projection.objective_stigmator) - projection.objective_stigmator *= 2 - projection.objective_stigmator /= 2 + 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) @@ -93,7 +93,7 @@ def test_acquisition(microscope: Microscope) -> None: print("\tFilm settings:", acquisition.film_settings) print("\tCameras:", cameras) - acquisition.screen = ScreenPosition.UP + acquisition.screen_position = ScreenPosition.UP for cam_name in cameras: image = acquisition.acquire_tem_image(cam_name,