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