├── .gitignore ├── .idea ├── git_toolbox_prj.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── pyTEM.iml └── vcs.xml ├── LICENSE ├── README.md ├── dist └── pyTEM-0.1.0-py3-none-any.whl ├── docs ├── A quantitative, space-resolved method for optical anisotropy estimation in bulk carbons.pdf ├── Original TEMPackage Scripting Guide -- Meaghen Jennings.pdf ├── TEM Advanced -- Thermo Fisher.pdf ├── TEM Basic -- Thermo Fisher.pdf └── pyTEM Class Diagram - Simple.jpg ├── earlier_versions ├── TemPackage.py └── microED_Tilt_Series.py ├── pyTEM ├── Interface.py ├── __init__.py ├── lib │ ├── Acquisition.py │ ├── AcquisitionSeries.py │ ├── RedirectStdStreams.py │ ├── StagePosition.py │ ├── __init__.py │ ├── blanker_control.py │ ├── hs_metadata_to_dict.py │ ├── make_dict_jsonable.py │ ├── mixins │ │ ├── AcquisitionMixin.py │ │ ├── BeamBlankerMixin.py │ │ ├── BeamShiftMixin.py │ │ ├── ImageShiftMixin.py │ │ ├── MagnificationMixin.py │ │ ├── ModeMixin.py │ │ ├── ScreenMixin.py │ │ ├── StageMixin.py │ │ ├── VacuumMixin.py │ │ └── __init__.py │ ├── pascal_to_log.py │ ├── rgb_to_greyscale.py │ ├── stock_mrc_extended_header │ │ ├── __init__.py │ │ ├── get_stock_mrc_header.py │ │ └── stock_mrc_extended_header.npy │ ├── tem_tilt_speed.py │ └── tilt_control.py └── test │ ├── StagePosition_testing.py │ ├── TEMInterface_testing_stage_movements.py │ ├── __init__.py │ ├── acquisition_timing │ └── acquisition_timing.txt │ ├── array_size.py │ ├── blanker_tilt_control.py │ ├── cv2_test.py │ ├── epoch_to_datetime.py │ ├── find_path.py │ ├── flip_and_rotate.py │ ├── hyperspy_test.py │ ├── image_shift_calibration │ ├── Image Shift Calibration.xlsx │ ├── __init__.py │ └── image_shift_calibration.py │ ├── investigate_mrc_files.py │ ├── investigate_test_images.py │ ├── list_indexing.py │ ├── mixins │ ├── BigClass.py │ ├── HelperMixIn1.py │ ├── HelperMixIn2.py │ └── __init__.py │ ├── mrcfile_header_testing.py │ ├── multitasking_testing.py │ ├── readme_example_code.py │ ├── serialem_test.py │ ├── synchronizing_threads.py │ ├── tif_stack_testing.py │ └── timing_timing.py ├── pyTEM_scripts ├── __init__.py ├── align_images.py ├── bulk_carbon_analysis.py ├── lib │ ├── __init__.py │ ├── align_images │ │ ├── GetInOutFile.py │ │ ├── __init__.py │ │ └── display_goodbye_message.py │ ├── bulk_carbon_analysis │ │ ├── GetCorrectionalFactors.py │ │ ├── GetInOutFiles.py │ │ ├── __init__.py │ │ └── display_goodbye_message.py │ └── micro_ed │ │ ├── __init__.py │ │ ├── add_basf_icon_to_tkinter_window.py │ │ ├── build_full_shifts_array.py │ │ ├── closest_number.py │ │ ├── exit_script.py │ │ ├── find_bound_indicies.py │ │ ├── hyperspy_warnings.py │ │ ├── ico │ │ ├── BASF.ico │ │ └── __init__.py │ │ ├── messages.py │ │ ├── obtain_shifts.py │ │ ├── opposite_signs.py │ │ ├── powspace.py │ │ └── user_inputs.py ├── micro_ed.py └── test │ ├── __init__.py │ ├── argparse_test.py │ └── micro_ed │ ├── AcquisitionSeriesProperties.py │ ├── __init__.py │ ├── counting_optima.py │ ├── perform_tilt_series.py │ ├── tilt_acq_timing │ └── acq_timing.txt │ ├── tilt_speed_calibration │ ├── tilt_speed.txt │ └── tilt_speed_calibration.xlsx │ └── tkinter_test.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Test and calibration data 2 | pyTEM/test/data/ 3 | pyTEM/test/TEMInferace Testing Log.xlsx 4 | pyTEM/test/image_shift_calibration/Calibration_Data/ 5 | pyTEM/test/image_shift_calibration/Image Shift Calibration.xlsx 6 | pyTEM/test/test_images/ 7 | 8 | pyTEM_scripts/test/micro_ed/check_message_ordering.py 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | pytem_dependencies/ 33 | 34 | # User-specific stuff 35 | .idea/**/workspace.xml 36 | .idea/**/tasks.xml 37 | .idea/**/usage.statistics.xml 38 | .idea/**/dictionaries 39 | .idea/**/shelf 40 | 41 | # Sensitive or high-churn files 42 | .idea/**/dataSources/ 43 | .idea/**/dataSources.ids 44 | .idea/**/dataSources.local.xml 45 | .idea/**/sqlDataSources.xml 46 | .idea/**/dynamic.xml 47 | .idea/**/uiDesigner.xml 48 | .idea/**/dbnavigator.xml 49 | 50 | # pyenv 51 | .python-version 52 | -------------------------------------------------------------------------------- /.idea/git_toolbox_prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pyTEM.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 RAA-OS Apps / xEM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/pyTEM-0.1.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/dist/pyTEM-0.1.0-py3-none-any.whl -------------------------------------------------------------------------------- /docs/A quantitative, space-resolved method for optical anisotropy estimation in bulk carbons.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/docs/A quantitative, space-resolved method for optical anisotropy estimation in bulk carbons.pdf -------------------------------------------------------------------------------- /docs/Original TEMPackage Scripting Guide -- Meaghen Jennings.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/docs/Original TEMPackage Scripting Guide -- Meaghen Jennings.pdf -------------------------------------------------------------------------------- /docs/TEM Advanced -- Thermo Fisher.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/docs/TEM Advanced -- Thermo Fisher.pdf -------------------------------------------------------------------------------- /docs/TEM Basic -- Thermo Fisher.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/docs/TEM Basic -- Thermo Fisher.pdf -------------------------------------------------------------------------------- /docs/pyTEM Class Diagram - Simple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/docs/pyTEM Class Diagram - Simple.jpg -------------------------------------------------------------------------------- /pyTEM/Interface.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | 5 | This is BASF's in-house TEM scripting interface. Bolted directly on top of a COM interface, this is just a Python 6 | wrapper for the scripting interface of Thermo Fisher Scientific and FEI microscopes. 7 | 8 | This is not a complete interface in that it does not provide access to all the functionality of the underlying Thermo 9 | Fisher Scientific and FEI scripting interface. However, it provides access to all basic microscope functions as well 10 | as all those required by other pyTEM automation pyTEM_scripts. 11 | """ 12 | 13 | import math 14 | import warnings 15 | 16 | import numpy as np 17 | import comtypes.client as cc 18 | 19 | # Mixins 20 | from pyTEM.lib.mixins.AcquisitionMixin import AcquisitionMixin 21 | from pyTEM.lib.mixins.MagnificationMixin import MagnificationMixin 22 | from pyTEM.lib.mixins.ImageShiftMixin import ImageShiftMixin 23 | from pyTEM.lib.mixins.BeamShiftMixin import BeamShiftMixin 24 | from pyTEM.lib.mixins.ModeMixin import ModeMixin 25 | from pyTEM.lib.mixins.ScreenMixin import ScreenMixin 26 | from pyTEM.lib.mixins.BeamBlankerMixin import BeamBlankerMixin 27 | from pyTEM.lib.mixins.StageMixin import StageMixin 28 | from pyTEM.lib.mixins.VacuumMixin import VacuumMixin 29 | 30 | # Other library imports 31 | from pyTEM.lib.StagePosition import StagePosition 32 | 33 | 34 | class Interface(AcquisitionMixin, # Microscope acquisition controls, including those for taking images. 35 | MagnificationMixin, # Magnification controls. 36 | ImageShiftMixin, # Image shift controls. 37 | BeamShiftMixin, # Beam Shift controls. 38 | ModeMixin, # Microscope mode controls, including those for projection and illuminations. 39 | ScreenMixin, # Microscope FluCam screen controls. 40 | BeamBlankerMixin, # Blank/unblank the beam. 41 | StageMixin, # Stage controls. 42 | VacuumMixin, # Vacuum system controls and pressure readouts. 43 | ): 44 | """ 45 | An interface for the Thermo-Fisher TEM microscopes. 46 | 47 | This is basically just a "user-friendly" wrapper for the Thermo-Fisher 'Instrument' object that is used to 48 | actually interact with and control the microscope. Note that there is a COM interface (a middle man of sorts) 49 | facilitating communication between the "user-friendly" wrapper and the underlying Thermo-Fisher 'Instrument' 50 | object. 51 | 52 | Note when adding mixins that those which include other mixins must be listed before those which they include. 53 | 54 | Public Attributes: 55 | None. 56 | 57 | Protected Attributes: 58 | _tem: Thermo Fisher 'Instrument' object: 59 | This is used "under-the-hood" to actually interact with and control a subset the microscope's features 60 | _tem_advanced: Thermo Fisher 'AdvancedInstrument' object 61 | This is used "under-the-hood" to actually interact with and control a remainder of the microscope's 62 | features. 63 | _stage_position: An instance of the simplified, forward facing StagePosition class: 64 | The current stage position. 65 | _image_shift_matrix: 2D numpy.ndarray: 66 | Matrix used to translate between the stage-plane and the image-plane. 67 | This matrix is based on a manual calibration, it is not perfect, but it does a reasonable job. 68 | _beam_shift_matrix: 2D numpy.ndarray: 69 | Matrix used to translate between the stage-plane and the beam-plane. 70 | This matrix is based on the image shift matrix. 71 | 72 | Private Attributes: 73 | None. Note: pyTEM attributes required by another mixin cannot be made private otherwise the mixins 74 | will not be able to access them through the COM interface. 75 | """ 76 | 77 | def __init__(self): 78 | try: 79 | self._tem = cc.CreateObject("TEMScripting.Instrument") 80 | self._tem_advanced = cc.CreateObject("TEMAdvancedScripting.AdvancedInstrument") 81 | except OSError as e: 82 | print("Unable to connect to microscope.") 83 | raise e 84 | 85 | scope_position = self._tem.Stage.Position # ThermoFisher StagePosition object 86 | 87 | self._stage_position = StagePosition(x=scope_position.X * 1e6, # m -> um 88 | y=scope_position.Y * 1e6, # m -> um 89 | z=scope_position.Z * 1e6, # m -> um 90 | alpha=math.degrees(scope_position.A), # rad -> deg 91 | beta=math.degrees(scope_position.B)) # rad -> deg 92 | 93 | self._image_shift_matrix = np.asarray([[1.010973981, 0.54071542], 94 | [-0.54071542, 1.010973981]]) 95 | 96 | # TODO: Perform some experiment to validate this beam shift matrix 97 | self._beam_shift_matrix = np.asarray([[1.010973981, 0.54071542], 98 | [0.54071542, -1.010973981]]) 99 | 100 | def normalize(self) -> None: 101 | """ 102 | Normalizes all lenses. 103 | :return: None. 104 | """ 105 | self._tem.NormalizeAll() 106 | 107 | def make_safe(self) -> None: 108 | """ 109 | Return the microscope to a safe state. 110 | :return: None. 111 | """ 112 | self.close_column_valve() 113 | self.blank_beam() 114 | self.insert_screen() 115 | 116 | def prepare_for_holder_removal(self) -> None: 117 | """ 118 | Prepare the TEM for holder removal. 119 | :return: None. 120 | """ 121 | self.close_column_valve() 122 | 123 | # Sometimes it takes a couple tries to fully reset the stage 124 | num_tries = 3 125 | for t in range(num_tries): 126 | if self.stage_is_home(): 127 | break # Then we are good 128 | else: 129 | self.reset_stage_position() # Try to send it home 130 | 131 | if t == num_tries - 1: 132 | # Then the stage has still not gone back, and we are out of tries, panik 133 | raise Exception("Unable to reset the stage.") 134 | 135 | self.set_image_shift(x=0, y=0) 136 | self.set_beam_shift(x=0, y=0) 137 | 138 | # Set the magnification somewhere in the SA range 139 | if self.get_mode() == "TEM": 140 | self.set_tem_magnification(new_magnification_index=23) # 8600.0 x Zoom 141 | elif self.get_mode() == "STEM": 142 | self.set_stem_magnification(new_magnification=8600.0) 143 | else: 144 | raise Exception("Error: Current microscope mode unknown.") 145 | 146 | warnings.warn("prepare_for_holder_removal() not fully implemented, please retract the camera " 147 | "before removing the holder.") # TODO: Retract camera 148 | 149 | def zero_shifts(self): 150 | """ 151 | Zero the image and beam shifts. 152 | :return: None. 153 | """ 154 | self.set_beam_shift(x=0, y=0) 155 | self.set_image_shift(x=0, y=0) 156 | 157 | 158 | if __name__ == "__main__": 159 | scope = Interface() 160 | -------------------------------------------------------------------------------- /pyTEM/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/__init__.py -------------------------------------------------------------------------------- /pyTEM/lib/RedirectStdStreams.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Rob Cowie, source pulled from stackoverflow 3 | """ 4 | 5 | import sys 6 | 7 | 8 | class RedirectStdStreams(object): 9 | def __init__(self, stdout=None, stderr=None): 10 | self._stdout = stdout or sys.stdout 11 | self._stderr = stderr or sys.stderr 12 | 13 | def __enter__(self): 14 | self.old_stdout, self.old_stderr = sys.stdout, sys.stderr 15 | self.old_stdout.flush() 16 | self.old_stderr.flush() 17 | sys.stdout, sys.stderr = self._stdout, self._stderr 18 | 19 | def __exit__(self, exc_type, exc_value, traceback): 20 | self._stdout.flush() 21 | self._stderr.flush() 22 | sys.stdout = self.old_stdout 23 | sys.stderr = self.old_stderr 24 | -------------------------------------------------------------------------------- /pyTEM/lib/StagePosition.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | 7 | class StagePosition: 8 | """ 9 | A simplified, forward facing StagePosition class. 10 | 11 | Private Attributes: 12 | __x: float: The current stage position along the x-axis, in micrometres. 13 | __y: float: The current stage position along the y-axis, in micrometres. 14 | __z: float: The current stage position along the z-axis, in micrometres. 15 | __alpha: float: The current stage tilt along the alpha direction, in degrees. 16 | __beta: float: The current stage tilt along the beta direction, in degrees. 17 | """ 18 | 19 | def __init__(self, x: float, y: float, z: float, alpha: float, beta: float): 20 | self.__x = x 21 | self.__y = y 22 | self.__z = z 23 | self.__alpha = alpha 24 | self.__beta = beta 25 | 26 | def get_x(self) -> float: 27 | """ 28 | :return: float: The current stage position along the x-axis, in micrometres. 29 | """ 30 | return self.__x 31 | 32 | def get_y(self) -> float: 33 | """ 34 | :return: float: The current stage position along the y-axis, in micrometres. 35 | """ 36 | return self.__y 37 | 38 | def get_z(self) -> float: 39 | """ 40 | :return: float: The current stage position along the z-axis, in micrometres. 41 | """ 42 | return self.__z 43 | 44 | def get_alpha(self) -> float: 45 | """ 46 | :return: float: The current stage tilt along the alpha direction, in degrees. 47 | """ 48 | return self.__alpha 49 | 50 | def get_beta(self) -> float: 51 | """ 52 | :return: float: The current stage tilt along the beta direction, in degrees. 53 | """ 54 | return self.__beta 55 | 56 | def set_x(self, new_x: float) -> None: 57 | """ 58 | Update the current stage position along the x-axis, in micrometres. 59 | Notice that since this does nothing to update the microscope's internal (Thermo-Fisher's) StagePosition object, 60 | utilization of this method will not affect the microscope. 61 | """ 62 | self.__x = new_x 63 | 64 | def set_y(self, new_y: float) -> None: 65 | """ 66 | Update the current stage position along the y-axis, in micrometres. 67 | Notice that since this does nothing to update the microscope's internal (Thermo-Fisher's) StagePosition object, 68 | utilization of this method will not affect the microscope. 69 | """ 70 | self.__y = new_y 71 | 72 | def set_z(self, new_z: float) -> None: 73 | """ 74 | Update the current stage position along the z-axis, in micrometres. 75 | Notice that since this does nothing to update the microscope's internal (Thermo-Fisher's) StagePosition object, 76 | utilization of this method will not affect the microscope. 77 | """ 78 | self.__z = new_z 79 | 80 | def set_alpha(self, new_alpha: float) -> None: 81 | """ 82 | Update the current stage tilt along the alpha direction, in degrees. 83 | Notice that since this does nothing to update the microscope's internal (Thermo-Fisher's) StagePosition object, 84 | utilization of this method will not affect the microscope. 85 | """ 86 | self.__alpha = new_alpha 87 | 88 | def set_beta(self, new_beta: float) -> None: 89 | """ 90 | Update the current stage tilt along the beta direction, in degrees. 91 | Notice that since this does nothing to update the microscope's internal (Thermo-Fisher's) StagePosition object, 92 | utilization of this method will not affect the microscope. 93 | """ 94 | self.__beta = new_beta 95 | 96 | def __str__(self): 97 | return "-- Current Stage Position -- " \ 98 | "\nx=" + str(self.get_x()) + " \u03BCm" \ 99 | "\ny=" + str(self.get_y()) + " \u03BCm" \ 100 | "\nz=" + str(self.get_z()) + " \u03BCm" \ 101 | "\n\u03B1=" + str(self.get_alpha()) + " deg" \ 102 | "\n\u03B2=" + str(self.get_beta()) + " deg" 103 | 104 | 105 | if __name__ == "__main__": 106 | stage_pos = StagePosition(x=100.3, y=-234.2, z=15.0, alpha=45, beta=0.0) 107 | print(stage_pos) 108 | -------------------------------------------------------------------------------- /pyTEM/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/lib/__init__.py -------------------------------------------------------------------------------- /pyTEM/lib/blanker_control.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import time 7 | 8 | from numpy.typing import ArrayLike 9 | 10 | from pyTEM.lib.mixins.BeamBlankerMixin import BeamBlankerInterface 11 | 12 | 13 | def blanker_control(num_acquisitions: int, barriers: ArrayLike, exposure_time: float, verbose: bool = False) -> None: 14 | """ 15 | Support pyTEM.Interface.acquisition() with simultaneous blanker control. For more information on why this 16 | is helpful, please refer to pyTEM.Interface.acquisition(). 17 | 18 | This function is to be run in a parallel thread or process while an acquisition (or acquisition series) is being 19 | performed from the main thread/process. 20 | 21 | To ensure beam is unblanked in time, we unblank 0.025 seconds early. And to ensure the beam remains unblanked for 22 | the whole time the camera is recording, we re-blank 0.025 seconds late. This means the beam is unblanked for 23 | exposure_time + 0.05 seconds. 24 | 25 | :param num_acquisitions: int: 26 | The number of acquisitions to perform. Must be at least 1. 27 | :param barriers: Array of mp.Barrier: 28 | An array of barriers, these are used to synchronize with the main thread/process before each 29 | acquisition. 30 | If this is being run in a parallel thread, this should be an array of threading.Barrier objects or similar. And 31 | if it is being run in a parallel process then this should be an array of multiprocessing.Barrier objects or 32 | similar. Either way, the barriers need to have a party allocated for the process/thread in which this function 33 | is running. 34 | :param exposure_time: float: 35 | Exposure time for a single acquisition, in seconds. # TODO: Allow variable exposure times 36 | 37 | :param verbose: (optional; default is False): 38 | Print out extra information. Useful for debugging and timing analysis. 39 | 40 | :return: None. 41 | """ 42 | if num_acquisitions <= 0: 43 | raise Exception("Error: blanker_control() requires we perform at least one acquisition.") 44 | 45 | if len(barriers) != num_acquisitions: 46 | raise Exception("Error: blanker_control() didn't receive the expected number of barriers. Expected " 47 | + str(num_acquisitions) + ", but received " + str(len(barriers)) + ".") 48 | 49 | # Build an interface to access blanker controls. 50 | interface = BeamBlankerInterface() 51 | 52 | # Loop through the requested number of acquisitions. 53 | for i in range(num_acquisitions): 54 | 55 | barriers[i].wait() # Synchronize with the main thread/process. 56 | 57 | # Wait while the camera is blind. Notice we wait a little longer than the integration time, this is because 58 | # the acquisition command takes a little longer to issue than the unblank command. We should wait about an 59 | # extra 0.45 seconds, but we unblank 0.025 seconds early to ensure the beam is unblanked in time. 60 | time.sleep(0.425 + exposure_time) 61 | 62 | # Unblank while the acquisition is active. 63 | beam_unblank_time = time.time() 64 | interface.unblank_beam() # Command takes 0.15 s 65 | 66 | # Wait while the camera is recording. 67 | # Notice we wait a little extra just to be sure the beam is unblanked for whole time the camera is recording. 68 | time.sleep(0.025 + exposure_time) 69 | 70 | # Re-blank the beam. 71 | beam_reblank_time = time.time() 72 | interface.blank_beam() # Command takes 0.15 s 73 | 74 | if verbose: 75 | print("-- Timing results from blanker_control() for acquisition #" + str(i) + " --") 76 | 77 | print("\nIssued the command to unblanked the beam at: " + str(beam_unblank_time)) 78 | print("Issued the command to re-blanked the beam at: " + str(beam_reblank_time)) 79 | # Extra 0.15 for unblank command + 0.05 extra unblanked time 80 | print("Total time spent with the beam unblanked: " + str(beam_reblank_time - beam_unblank_time - 0.20)) 81 | -------------------------------------------------------------------------------- /pyTEM/lib/hs_metadata_to_dict.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | from hyperspy.misc.utils import DictionaryTreeBrowser 7 | 8 | 9 | def hs_metadata_to_dict(hs_dict: DictionaryTreeBrowser) -> dict: 10 | """ 11 | Convert Hyperspy metadata, which is a bunch of nested DictionaryTreeBrowser objects, to a normal Python dictionary. 12 | Conversion is performed recursively. 13 | 14 | :param hs_dict: DictionaryTreeBrowser: 15 | A Hyperspy DictionaryTreeBrowser object. 16 | 17 | :return: dict: 18 | The provided metadata as a normal python dictionary. 19 | """ 20 | normal_dict = dict(hs_dict) 21 | for key, value in normal_dict.items(): 22 | if isinstance(value, DictionaryTreeBrowser): 23 | # Then recursively run through converting all DictionaryTreeBrowser within.. 24 | normal_dict[key] = hs_metadata_to_dict(hs_dict=value) 25 | else: 26 | normal_dict[key] = value 27 | 28 | return normal_dict 29 | -------------------------------------------------------------------------------- /pyTEM/lib/make_dict_jsonable.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | import json 6 | 7 | 8 | def make_dict_jsonable(my_dict: dict) -> dict: 9 | """ 10 | Some types are not be JSON serializable. Given a dictionary, recursively run through and convert all non-JSON- 11 | serializable types to strings. 12 | 13 | :param my_dict: dict: 14 | A dictionary (possibly contining nested dictionaries), which may or may not contain non-JSON-serializable types. 15 | :return: dict: 16 | The provided dictionary, but will all non-JSON-serializable items converted to string. 17 | """ 18 | jsonable_dict = dict() 19 | for key, value in my_dict.items(): 20 | if isinstance(value, dict): 21 | # Then recursively run through looking for non jsonable values within.. 22 | jsonable_dict[key] = make_dict_jsonable(value) 23 | else: 24 | if is_jsonable(value): 25 | jsonable_dict[key] = value 26 | else: 27 | jsonable_dict[key] = str(value) 28 | 29 | return jsonable_dict 30 | 31 | 32 | def is_jsonable(x): 33 | """ 34 | Return True if x is JSON serializable, False otherwise. 35 | """ 36 | try: 37 | json.dumps(x) 38 | return True 39 | except (TypeError, OverflowError): 40 | return False 41 | -------------------------------------------------------------------------------- /pyTEM/lib/mixins/BeamBlankerMixin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import comtypes.client as cc 7 | 8 | 9 | class BeamBlankerMixin: 10 | """ 11 | Microscope beam blanker controls, including functions to blank and unblank the beam. 12 | This mixin was developed in support of pyTEM.Interface, but can be included in other projects where helpful. 13 | """ 14 | try: 15 | # Unresolved attribute warning suppression 16 | _tem: type(cc.CreateObject("TEMScripting.Instrument")) 17 | except OSError: 18 | pass 19 | 20 | def beam_is_blank(self) -> bool: 21 | """ 22 | Check if the beam is blanked. 23 | :return: Beam status: bool: 24 | True: Beam is blanked. 25 | False: Beam is unblanked. 26 | """ 27 | return self._tem.Illumination.BeamBlanked 28 | 29 | def blank_beam(self) -> None: 30 | """ 31 | Blank the beam. 32 | :return: None. 33 | """ 34 | if self.beam_is_blank(): 35 | print("The beam is already blanked.. no changes made.") 36 | return 37 | 38 | # Go ahead and blank the beam 39 | self._tem.Illumination.BeamBlanked = True 40 | 41 | def unblank_beam(self) -> None: 42 | """ 43 | Unblank the beam. 44 | :return: None. 45 | """ 46 | if not self.beam_is_blank(): 47 | print("The beam is already unblanked.. no changes made.") 48 | return 49 | 50 | self._tem.Illumination.BeamBlanked = False 51 | 52 | 53 | class BeamBlankerInterface(BeamBlankerMixin): 54 | """ 55 | A microscope interface with only beam blanker controls. 56 | """ 57 | 58 | def __init__(self): 59 | try: 60 | self._tem = cc.CreateObject("TEMScripting.Instrument") 61 | except OSError as e: 62 | print("Unable to connect to the microscope.") 63 | raise e 64 | -------------------------------------------------------------------------------- /pyTEM/lib/mixins/BeamShiftMixin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import warnings 7 | import numpy as np 8 | import comtypes.client as cc 9 | 10 | from numpy.typing import ArrayLike 11 | 12 | from pyTEM.lib.mixins.ModeMixin import ModeMixin 13 | 14 | # TODO: Still requires testing 15 | 16 | 17 | class BeamShiftMixin(ModeMixin): 18 | """ 19 | Microscope beam shift controls. 20 | 21 | This mixin was developed in support of pyTEM.Interface, but can be included in other projects where helpful. 22 | """ 23 | try: 24 | # Unresolved attribute warning suppression 25 | _tem: type(cc.CreateObject("TEMScripting.Instrument")) 26 | except OSError: 27 | pass 28 | _beam_shift_matrix: type(np.empty(shape=(2, 2))) 29 | 30 | # Note: For precise shifting, an offset vector is required to account for hysteresis in the len's magnets. However, 31 | # this is negligible for most applications. 32 | # _beam_shift_vector: type(np.empty(shape=(2, 1))) 33 | 34 | def get_beam_shift_matrix(self) -> np.ndarray: 35 | """ 36 | :return: numpy.ndarray 37 | The new 2x2 matrix used to translate between the stage-plane and the beam-plane 38 | """ 39 | return self._beam_shift_matrix 40 | 41 | def _set_beam_shift_matrix(self, a: ArrayLike) -> None: 42 | """ 43 | Update the beam shift matrix. 44 | :param a: numpy.ndarray: 45 | The new 2x2 used to translate between the stage-plane and the beam-plane. 46 | :return: None 47 | """ 48 | self._stage_position = a 49 | 50 | def get_beam_shift(self) -> np.array: 51 | """ 52 | :return: [x, y]: 2-element numpy.array: 53 | The x and y values of the beam shift, in micrometres. 54 | """ 55 | if self._tem.Projection.SubModeString != "SA": 56 | warnings.warn("Beam shift functions only tested for magnifications in SA range (4300 x -> 630 kx Zoom), " 57 | "but the current projection submode is " + self._tem.Projection.SubModeString + 58 | ". Magnifications in this range may require a different transformation matrix.") 59 | 60 | beam_shift = self._tem.Illumination.Shift 61 | 62 | # Notice we need to use the beam shift matrix to translate from the beam plane back to the stage plane. 63 | # Beam shift along y is in the wrong direction (IDK why), for now we just invert the user input. 64 | temp = 1e6 * np.matmul(self.get_beam_shift_matrix(), np.asarray([beam_shift.X, beam_shift.Y])) 65 | return np.asarray([temp[0], - temp[1]]) 66 | 67 | def set_beam_shift(self, x: float = None, y: float = None) -> None: 68 | """ 69 | Move the beam shift to the provided x and y values. 70 | 71 | :param x: float: 72 | The new beam shift location, along the x-axis, in micrometres. 73 | :param y: 74 | The new beam shift location, along the y-axis, in micrometres. 75 | 76 | :return: None 77 | """ 78 | if self.get_projection_submode() != "SA": 79 | warnings.warn("Beam shift functions only tested for magnifications in SA range (4300 x -> 630 kx Zoom), " 80 | "but the current projection submode is " + self._tem.Projection.SubModeString + 81 | ". Magnifications in this range may require a different transformation matrix.") 82 | 83 | a = self.get_beam_shift_matrix() 84 | 85 | u = self.get_beam_shift() # Start from the current beam shift location 86 | 87 | if x is not None: 88 | u[0] = x / 1e6 # Update our x-value (convert um -> m) 89 | if y is not None: 90 | # Beam shift along y is in the wrong direction (IDK why), for now we just invert the user input. 91 | u[1] = -y / 1e6 # update our y-value (convert um -> m) 92 | 93 | # Translate back to the microscope plane and perform the shift 94 | u_prime = np.matmul(np.linalg.inv(a), u) 95 | new_beam_shift = self._tem.Illumination.Shift # Just to get the required ThermoFisher Vector object 96 | new_beam_shift.X = u_prime[0] 97 | new_beam_shift.Y = u_prime[1] 98 | self._tem.Illumination.Shift = new_beam_shift 99 | 100 | 101 | class BeamShiftInterface(BeamShiftMixin): 102 | """ 103 | A microscope interface with only beam shift controls. 104 | """ 105 | 106 | def __init__(self): 107 | try: 108 | self._tem = cc.CreateObject("TEMScripting.Instrument") 109 | except OSError as e: 110 | print("Unable to connect to the microscope.") 111 | raise e 112 | -------------------------------------------------------------------------------- /pyTEM/lib/mixins/ImageShiftMixin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import warnings 7 | import numpy as np 8 | import comtypes.client as cc 9 | 10 | from numpy.typing import ArrayLike 11 | 12 | from pyTEM.lib.mixins.ModeMixin import ModeMixin 13 | 14 | 15 | class ImageShiftMixin(ModeMixin): 16 | """ 17 | Microscope image shift controls. 18 | 19 | This mixin was developed in support of pyTEM.Interface, but can be included in other projects where helpful. 20 | """ 21 | try: 22 | # Unresolved attribute warning suppression 23 | _tem: type(cc.CreateObject("TEMScripting.Instrument")) 24 | except OSError: 25 | pass 26 | _image_shift_matrix: type(np.empty(shape=(2, 2))) 27 | 28 | # Note: For precise shifting, an offset vector is required to account for hysteresis in the len's magnets. However, 29 | # this is negligible for most applications. 30 | # _image_shift_vector: type(np.empty(shape=(2, 1))) 31 | 32 | def get_image_shift_matrix(self) -> np.ndarray: 33 | """ 34 | :return: numpy.ndarray: 35 | The 2x2 matrix used to translate between the stage-plane and the image-plane. 36 | """ 37 | return self._image_shift_matrix 38 | 39 | def _set_image_shift_matrix(self, a: ArrayLike) -> None: 40 | """ 41 | Update the image shift matrix. 42 | :param a: numpy.ndarray: 43 | The new 2x2 matrix used to translate between the stage-plane and the image-plane. 44 | :return: None. 45 | """ 46 | self._stage_position = a 47 | 48 | def get_image_shift(self) -> np.array: 49 | """ 50 | :return: [x, y]: 2-element numpy.array: 51 | The x and y values of the image shift, in micrometres. 52 | """ 53 | if self.get_projection_mode() == "imaging" and self.get_projection_submode() != "SA": 54 | warnings.warn("Image shift functions only tested for magnifications in SA range (4300 x -> 630 kx Zoom), " 55 | "but the current projection submode is " + self._tem.Projection.SubModeString + 56 | ". Magnifications in this range may require a different transformation matrix.") 57 | 58 | image_shift = self._tem.Projection.ImageShift 59 | 60 | # Notice we need to use the image shift matrix to translate from the image plane back to the stage plane. 61 | # Image shift along y is in the wrong direction (IDK why), for now we just invert the user input. 62 | temp = 1e6 * np.matmul(self.get_image_shift_matrix(), np.asarray([image_shift.X, image_shift.Y])) 63 | return np.asarray([temp[0], - temp[1]]) 64 | 65 | def set_image_shift(self, x: float = None, y: float = None) -> None: 66 | """ 67 | Move the image shift to the provided x and y values. 68 | 69 | :param x: float: 70 | The new image shift location, along the x-axis, in micrometres. 71 | :param y: 72 | The new image shift location, along the y-axis, in micrometres. 73 | 74 | :return: None. 75 | """ 76 | if self.get_projection_submode() != "SA": 77 | warnings.warn("Image shift functions only tested for magnifications in SA range (4300 x -> 630 kx Zoom), " 78 | "but the current projection submode is " + self._tem.Projection.SubModeString + 79 | ". Magnifications in this range may require a different transformation matrix.") 80 | 81 | a = self.get_image_shift_matrix() 82 | 83 | u = self.get_image_shift() # Start from the current image shift location 84 | 85 | if x is not None: 86 | u[0] = x / 1e6 # Update our x-value (convert um -> m) 87 | if y is not None: 88 | # Image shift along y is in the wrong direction (IDK why), for now we just invert the user input. 89 | u[1] = -y / 1e6 # update our y-value (convert um -> m) 90 | 91 | # Translate back to the microscope plane and perform the shift 92 | u_prime = np.matmul(np.linalg.inv(a), u) 93 | new_image_shift = self._tem.Projection.ImageShift # Just to get the required ThermoFisher Vector object 94 | new_image_shift.X = u_prime[0] 95 | new_image_shift.Y = u_prime[1] 96 | self._tem.Projection.ImageShift = new_image_shift 97 | 98 | 99 | class ImageShiftInterface(ImageShiftMixin): 100 | """ 101 | A microscope interface with only image shift (and be extension mode) controls. 102 | """ 103 | 104 | def __init__(self): 105 | try: 106 | self._tem = cc.CreateObject("TEMScripting.Instrument") 107 | except OSError as e: 108 | print("Unable to connect to the microscope.") 109 | raise e 110 | -------------------------------------------------------------------------------- /pyTEM/lib/mixins/MagnificationMixin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import math 7 | import warnings 8 | import comtypes.client as cc 9 | 10 | from pyTEM.lib.mixins.ModeMixin import ModeMixin 11 | 12 | 13 | class MagnificationMixin(ModeMixin): 14 | """ 15 | Microscope magnification controls, including those for getting and setting the current microscope magnification and 16 | magnification index. 17 | 18 | This mixin was developed in support of pyTEM.Interface, but can be included in other projects where helpful. 19 | """ 20 | try: 21 | # Unresolved attribute warning suppression 22 | _tem: type(cc.CreateObject("TEMScripting.Instrument")) 23 | except OSError: 24 | pass 25 | 26 | def get_magnification(self) -> float: 27 | """ 28 | :return: float: 29 | The current magnification value. 30 | Note: Returns Nan when the instrument is in TEM diffraction mode. 31 | """ 32 | if self.get_mode() == "TEM": 33 | if self.get_projection_mode() == "imaging": 34 | return self._tem.Projection.Magnification 35 | elif self.get_projection_mode() == "diffraction": 36 | warnings.warn("Since we are in TEM diffraction mode, get_magnification() is returning Nan...") 37 | return math.nan 38 | else: 39 | raise Exception("Projection mode (" + str(self._tem.Projection.Mode) + ") not recognized.") 40 | 41 | elif self.get_mode() == "STEM": 42 | # TODO: Figure out how to reliably obtain the magnification while in STEM mode. 43 | warnings.warn("get_magnification() is not working as expected while the instrument is in STEM mode.") 44 | return self._tem.Illumination.StemMagnification 45 | 46 | else: 47 | raise Exception("Projection mode (" + str(self._tem.Projection.Mode) + ") not recognized.") 48 | 49 | def set_stem_magnification(self, new_magnification: float) -> None: 50 | """ 51 | Update the STEM microscope magnification. 52 | 53 | This method requires the microscope be in TEM mode. For magnification updates while in TEM mode, please use 54 | set_tem_magnification(). 55 | 56 | If the input is not one of the preset values, then the TEM will automatically set the magnification to the next 57 | nearest defined magnification value. 58 | 59 | :param new_magnification: float: 60 | The new STEM magnification. Available magnifications in STEM mode (SA) range from 4,300 to 630,000. 61 | :return: None. 62 | """ 63 | if self.get_mode() == "TEM": 64 | print("The microscope is currently in TEM mode. To adjust the magnification in TEM mode, please use " 65 | "set_magnification_tem().. no changes made.") 66 | 67 | elif self.get_mode() == "STEM": 68 | # TODO: Figure out how to reliably set the magnification while in STEM mode. 69 | warnings.warn("set_stem_magnification() is not working as expected. STEM magnification may or may not have" 70 | " been updated.") 71 | self._tem.Illumination.StemMagnification = new_magnification 72 | 73 | else: 74 | raise Exception("Error: Current microscope mode unknown.") 75 | 76 | def set_tem_magnification(self, new_magnification_index: int) -> None: 77 | """ 78 | Update the TEM microscope magnification. 79 | 80 | The method requires the microscope be in TEM mode. For magnification updates while in STEM mode, please use 81 | set_stem_magnification(). 82 | 83 | :param new_magnification_index: int: 84 | Magnification index; an integer value between 1 (25 x Zoom) and 44 (1.05 Mx Zoom). 85 | 86 | :return: None. 87 | """ 88 | new_magnification_index = int(new_magnification_index) 89 | if self.get_mode() == "STEM": 90 | print("The microscope is currently in STEM mode. To adjust the magnification in STEM mode, please use " 91 | "set_magnification_stem().. no changes made.") 92 | 93 | elif self.get_mode() == "TEM": 94 | if new_magnification_index > 44: # Upper bound (Very zoomed in; 1.05 Mx Zoom) 95 | warnings.warn( 96 | "The requested TEM magnification index (" + str(new_magnification_index) + ") is greater than " 97 | "the maximum allowable value of 44. Therefore, the magnification index is being set to 44 " 98 | "(1.05 Mx Zoom).") 99 | new_magnification_index = 44 100 | elif new_magnification_index < 1: # Lower bound (Very zoomed out; 25 x Zoom) 101 | warnings.warn("The requested TEM magnification index (" + str(new_magnification_index) + ") is less " 102 | "than the minimum allowable value of 1. Therefore, the magnification index is being set " 103 | "to 1 (25 x Zoom).") 104 | new_magnification_index = 1 105 | self._tem.Projection.MagnificationIndex = new_magnification_index 106 | 107 | else: 108 | raise Exception("Error: Current microscope mode unknown.") 109 | 110 | def shift_tem_magnification(self, magnification_shift: float) -> None: 111 | """ 112 | Shift the TEM magnification up or down by the provided magnification_shift. This is equivalent to turning the 113 | magnification nob by magnification_shift notches clockwise (negative values of magnification_shift are 114 | equivalent to counterclockwise nob rotations). 115 | 116 | :param magnification_shift: float 117 | An up/down shift from current magnification; Examples: 7, -2, 10 118 | Should the request shift exceed the magnification bounds (1 to 44), magnification will default to the 119 | nearest bound. 120 | 121 | :return: None. 122 | """ 123 | if self.get_mode() == "STEM": 124 | print("The microscope is currently in STEM mode. To adjust the magnification in STEM mode, please use " 125 | "set_magnification_stem().. no changes made.") 126 | 127 | elif self.get_mode() == "TEM": 128 | current_magnification_index = self.get_magnification_index() 129 | new_magnification_index = current_magnification_index + int(magnification_shift) 130 | 131 | if new_magnification_index > 44: # Upper bound (Very zoomed in; 1.05 Mx Zoom) 132 | warnings.warn("The requested TEM magnification index shift (" + str(magnification_shift) + ") would " 133 | "cause the instrument to exceed the maximum allowable value of 44. Therefore, the " 134 | "magnification index is being set to 44 (1.05 Mx Zoom).") 135 | new_magnification_index = 44 136 | elif new_magnification_index < 1: # Lower bound (Very zoomed out; 25 x Zoom) 137 | warnings.warn("The requested TEM magnification index shift (" + str(magnification_shift) + ") would " 138 | "cause the instrument to exceed the minimum allowable value of 1. Therefore, the " 139 | "magnification index is being set to 1 (25 x Zoom).") 140 | new_magnification_index = 1 141 | 142 | self._tem.Projection.MagnificationIndex = new_magnification_index 143 | 144 | else: 145 | raise Exception("Error: Current microscope mode unknown.") 146 | 147 | def get_magnification_index(self) -> int: 148 | """ 149 | :return: int: 150 | The magnification index (this is what sets the magnification when the microscope is in TEM mode). 151 | """ 152 | if self.get_mode() == "STEM": 153 | warnings.warn("Magnification index is not relevant for STEM mode.") 154 | 155 | return self._tem.Projection.MagnificationIndex 156 | 157 | def print_available_magnifications(self) -> None: 158 | """ 159 | Print a list of available magnifications (when in TEM Imaging mode, magnification indices are also printed out). 160 | 161 | :return: None. 162 | """ 163 | print("The microscope is currently in " + self.get_mode() + " " + self.get_projection_mode() + " mode. " 164 | "Available magnifications are as follows:") 165 | 166 | if self.get_mode() == "TEM": 167 | 168 | if self.get_projection_mode() == "imaging": 169 | available_magnifications = [25.0, 34.0, 46.0, 62.0, 84.0, 115.0, 155.0, 210.0, 280.0, 380.0, 170 | 510.0, 700.0, 940.0, 1300.0, 1700.0, 2300.0, 2050.0, 2600.0, 3300.0, 171 | 4300.0, 5500.0, 7000.0, 8600.0, 11000.0, 14000.0, 17500.0, 22500.0, 172 | 28500.0, 36000.0, 46000.0, 58000.0, 74000.0, 94000.0, 120000.0, 173 | 150000.0, 190000.0, 245000.0, 310000.0, 390000.0, 500000.0, 630000.0, 174 | 650000.0, 820000.0, 1050000.0] 175 | 176 | print("{:<20} {:<20}".format("Magnification Index", "Magnification [x Zoom]")) 177 | for i, magnification in enumerate(available_magnifications): 178 | print("{:<20} {:<20}".format(i + 1, magnification)) 179 | 180 | elif self.get_projection_mode() == "diffraction": 181 | warnings.warn("Magnification not relevant in TEM diffraction mode.") # TODO: Is this true? 182 | 183 | else: 184 | warnings.warn("Projection mode not recognized.. no available magnifications found.") 185 | 186 | elif self.get_mode() == "STEM": 187 | 188 | if self.get_projection_mode() == "imaging": 189 | available_magnifications = [4300.0, 5500.0, 7000.0, 8600.0, 11000.0, 14000.0, 17500.0, 22500.0, 190 | 28500.0, 36000.0, 46000.0, 58000.0, 74000.0, 94000.0, 120000.0, 191 | 150000.0, 190000.0, 245000.0, 310000.0, 390000.0, 500000.0, 630000.0] 192 | 193 | elif self.get_projection_mode() == "diffraction": 194 | available_magnifications = [320.0, 450.0, 630.0, 900.0, 1250.0, 1800.0, 2550.0, 3600.0, 5100.0, 7200.0, 195 | 10000.0, 14500.0, 20500.0, 28500.0, 41000.0, 57000.0, 81000.0, 115000.0, 196 | 160000.0, 230000.0, 320000.0, 460000.0, 650000.0, 920000.0, 1300000.0, 197 | 5000, 7000, 9900, 14000, 20000, 28000, 40000, 56000, 79000, 110000.0, 198 | 160000.0, 225000.0, 320000.0, 450000.0, 630000.0, 900000.0, 1250000.0, 199 | 1800000.0, 2550000.0, 3600000.0, 5100000.0, 7200000.0, 10000000.0, 200 | 14500000.0, 20500000.0, 28500000.0, 41000000.0, 57000000.0, 81000000.0, 201 | 115000000.0, 160000000.0, 230000000.0, 320000000.0] 202 | else: 203 | warnings.warn("Projection mode not recognized.. no available magnifications found.") 204 | available_magnifications = [] 205 | 206 | print("Magnification [x Zoom]") 207 | for magnification in available_magnifications: 208 | print(magnification) 209 | 210 | else: 211 | raise Exception("Error: Microscope mode unknown.") 212 | 213 | 214 | class MagnificationInterface(MagnificationMixin): 215 | """ 216 | A microscope interface with only magnification (and be extension mode) controls. 217 | """ 218 | 219 | def __init__(self): 220 | try: 221 | self._tem = cc.CreateObject("TEMScripting.Instrument") 222 | except OSError as e: 223 | print("Unable to connect to the microscope.") 224 | raise e 225 | -------------------------------------------------------------------------------- /pyTEM/lib/mixins/ModeMixin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import comtypes.client as cc 7 | 8 | from pyTEM.lib.mixins.ScreenMixin import ScreenMixin 9 | 10 | 11 | class ModeMixin(ScreenMixin): 12 | """ 13 | Microscope mode controls, including those for projection and illumination. 14 | 15 | This mixin was developed in support of pyTEM.Interface, but can be included in other projects where helpful. 16 | """ 17 | try: 18 | # Unresolved attribute warning suppression 19 | _tem: type(cc.CreateObject("TEMScripting.Instrument")) 20 | except OSError: 21 | pass 22 | 23 | def get_mode(self) -> str: 24 | """ 25 | :return: str: 26 | The current microscope mode, one of: 27 | - "TEM" (normal imaging mode) 28 | - "STEM" (Scanning TEM) 29 | """ 30 | if self._tem.InstrumentModeControl.InstrumentMode == 0: 31 | return "TEM" 32 | 33 | elif self._tem.InstrumentModeControl.InstrumentMode == 1: 34 | return "STEM" # Scanning TEM 35 | 36 | else: 37 | raise Exception("Error: Microscope mode unknown.") 38 | 39 | def set_mode(self, new_mode: str) -> None: 40 | """ 41 | Change the operational mode of the microscope. 42 | 43 | :param new_mode: str: 44 | The new microscope mode, one of: 45 | - "STEM" (scanning TEM; the beam is focused at a large angle and is converged into a focal point) 46 | - "TEM" (parallel electron beams are focused perpendicular to the sample plane) 47 | :return: None. 48 | """ 49 | new_mode = str(new_mode).upper() 50 | 51 | if self.get_mode() == new_mode: 52 | print("The microscope is already in '" + new_mode + "' mode.. no changes made.") 53 | return 54 | 55 | if new_mode == "TEM": 56 | self._tem.InstrumentModeControl.InstrumentMode = 0 # Switch microscope into TEM Mode (normal imaging mode) 57 | 58 | elif new_mode == "STEM": 59 | self._tem.InstrumentModeControl.InstrumentMode = 1 # Switch microscope into STEM Mode (scanning TEM) 60 | 61 | else: 62 | print("The requested mode (" + new_mode + ") isn't recognized.. no changes made.") 63 | 64 | def get_projection_mode(self) -> str: 65 | """ 66 | :return: str: 67 | The current projection mode, either "diffraction" or "imaging". 68 | """ 69 | if self._tem.Projection.Mode == 1: 70 | return "imaging" 71 | 72 | elif self._tem.Projection.Mode == 2: 73 | return "diffraction" 74 | 75 | else: 76 | raise Exception("Error: Projection mode unknown.") 77 | 78 | def set_projection_mode(self, new_projection_mode: str) -> None: 79 | """ 80 | :param new_projection_mode: str: 81 | The new projection mode, one of: 82 | - "diffraction" (reciprocal space) 83 | - "imaging" (real space) 84 | :return: None 85 | """ 86 | new_projection_mode = str(new_projection_mode).title() 87 | 88 | if self.get_projection_mode() == new_projection_mode.title(): 89 | print("The microscope is already in '" + new_projection_mode + "' mode.. no changes made.") 90 | return 91 | 92 | user_screen_position = self.get_screen_position() 93 | 94 | if user_screen_position == 'retracted': 95 | # Insert the screen before switching moves to avoid damaging the camera 96 | self.insert_screen() 97 | 98 | if new_projection_mode.lower() in {"imaging", "i"}: 99 | self._tem.Projection.Mode = 1 # Switch microscope into imaging mode 100 | 101 | elif new_projection_mode.lower() in {"diffraction", "d"}: 102 | self._tem.Projection.Mode = 2 # Switch microscope into diffraction mode 103 | 104 | else: 105 | print("The requested projection mode (" + new_projection_mode + ") isn't recognized.. no changes made.") 106 | 107 | if user_screen_position == 'retracted': 108 | # Put the screen back where the user had it 109 | self.retract_screen() 110 | 111 | def get_projection_submode(self) -> str: 112 | """ 113 | :return: str: 114 | The current projection sub-mode. 115 | """ 116 | return self._tem.Projection.SubModeString 117 | 118 | def print_projection_submode(self): 119 | """ 120 | :return: str: 121 | The current projection submode, along with the zoom range. 122 | """ 123 | submode = self._tem.Projection.SubMode 124 | if submode == 1: 125 | print("LM: 25 x -> 2300 x Zoom") 126 | elif submode == 2: 127 | print("M: 2050 x -> 3300 x Zoom") 128 | elif submode == 3: 129 | print("SA: 4300 x -> 630 kx Zoom") # When in STEM mode, this is the only allowed submode 130 | elif submode == 4: 131 | print("MH: 650 kx -> 1.05 Mx Zoom") 132 | elif submode == 5: 133 | print("LAD: 4.6 m -> 1,400 m Diffraction") 134 | elif submode == 6: 135 | print("D: 14 mm -> 5.7 m Diffraction") 136 | else: 137 | print("Submode " + str(submode) + " (" + str(self._tem.Projection.SubModeString) + ") not recognized.") 138 | 139 | def get_illumination_mode(self): 140 | """ 141 | :return: str: 142 | The current illumination mode, one of: 143 | - "nanoprobe" (used to get a small convergent electron beam) 144 | - "microprobe" (provides a nearly parallel illumination at the cost of a larger probe size) 145 | """ 146 | if self._tem.Illumination.Mode == 0: 147 | return "nanoprobe" 148 | 149 | elif self._tem.Illumination.Mode == 1: 150 | return "microprobe" 151 | 152 | else: 153 | raise Exception("Error: Projection mode '" + str(self._tem.Illumination.Mode) + "' not recognized.") 154 | 155 | def set_illumination_mode(self, new_mode: str) -> None: 156 | """ 157 | Change the illumination mode of the microscope. 158 | Note: (Nearly) no effect for low magnifications (LM). 159 | 160 | :param new_mode: str: 161 | The new illumination mode, one of: 162 | - "nanoprobe" (used to get a small convergent electron beam) 163 | - "microprobe" (provides a nearly parallel illumination at the cost of a larger probe size) 164 | :return: None. 165 | """ 166 | new_mode = str(new_mode).title() 167 | 168 | if self.get_illumination_mode() == new_mode: 169 | print("The microscope is already in '" + new_mode + "' mode.. no changes made.") 170 | return 171 | 172 | if new_mode == "nanoprobe": 173 | self._tem.Illumination.Mode = 0 174 | 175 | elif new_mode == "microprobe": 176 | self._tem.Illumination.Mode = 1 177 | 178 | else: 179 | print("The requested illumination mode (" + new_mode + ") isn't recognized.. no changes made.") 180 | 181 | 182 | class ModeInterface(ModeMixin): 183 | """ 184 | A microscope interface with only mode (and by extension screen) controls. 185 | """ 186 | 187 | def __init__(self): 188 | try: 189 | self._tem = cc.CreateObject("TEMScripting.Instrument") 190 | except OSError as e: 191 | print("Unable to connect to the microscope.") 192 | raise e 193 | -------------------------------------------------------------------------------- /pyTEM/lib/mixins/ScreenMixin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import comtypes.client as cc 7 | 8 | 9 | class ScreenMixin: 10 | """ 11 | Microscope FluCam screen controls. 12 | 13 | This mixin was developed in support of pyTEM.Interface, but can be included in other projects where helpful. 14 | """ 15 | try: 16 | # Unresolved attribute warning suppression 17 | _tem: type(cc.CreateObject("TEMScripting.Instrument")) 18 | except OSError: 19 | pass 20 | 21 | def get_screen_position(self) -> str: 22 | """ 23 | :return: str: The position of the FluCam's fluorescent screen, one of: 24 | - 'retracted' (required to take images) 25 | - 'inserted' (required to use the FluCam to view the live image) 26 | """ 27 | if self._tem.Camera.MainScreen == 2: 28 | return "retracted" 29 | 30 | elif self._tem.Camera.MainScreen == 3: 31 | return "inserted" 32 | 33 | else: 34 | raise Exception("Error: Current screen position (" + str(self._tem.Camera.MainScreen) + ") not recognized.") 35 | 36 | def insert_screen(self) -> None: 37 | """ 38 | Insert the FluCam's fluorescent screen. 39 | This is required to use the FluCam to view the live image. 40 | :return: None. 41 | """ 42 | if self.get_screen_position() == "inserted": 43 | print("The microscope screen is already inserted.. no changes made.") 44 | return 45 | 46 | self._tem.Camera.MainScreen = 3 # Insert the screen 47 | 48 | def retract_screen(self) -> None: 49 | """ 50 | Retract the FluCam's fluorescent screen. 51 | This is required to take images. 52 | :return: None. 53 | """ 54 | if self.get_screen_position() == "retracted": 55 | print("The microscope screen is already removed.. no changes made.") 56 | return 57 | 58 | self._tem.Camera.MainScreen = 2 # Remove the screen 59 | 60 | 61 | class ScreenMixinInterface(ScreenMixin): 62 | """ 63 | A microscope interface with only screen controls. 64 | """ 65 | 66 | def __init__(self): 67 | try: 68 | self._tem = cc.CreateObject("TEMScripting.Instrument") 69 | except OSError as e: 70 | print("Unable to connect to the microscope.") 71 | raise e 72 | -------------------------------------------------------------------------------- /pyTEM/lib/mixins/VacuumMixin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import math 7 | from typing import Dict, List, Union 8 | 9 | import comtypes.client as cc 10 | 11 | from pyTEM.lib.pascal_to_log import pascal_to_log # Requires the pyTEM package directory on path 12 | 13 | 14 | class VacuumMixin: 15 | """ 16 | Microscope vacuum system valve controls and pressure reports. 17 | 18 | This mixin was developed in support of pyTEM.Interface, but can be included in other projects where helpful. 19 | """ 20 | try: 21 | # Unresolved attribute warning suppression 22 | _tem: type(cc.CreateObject("TEMScripting.Instrument")) 23 | except OSError: 24 | pass 25 | 26 | def _pull_vacuum_info(self) -> Dict[int, List[Union[str, float]]]: 27 | """ 28 | Pull vacuum info from the microscope and, from this info, build and return a dictionary (keyed by integers) 29 | containing 3-element lists with gauge names, pressures in Pascals, and pressures in log units. 30 | 31 | :return: Vacuum info: dictionary: 32 | Keys are integers, 3-element lists containing gauge name, pressure in Pascals, and pressure in log units. 33 | """ 34 | # The gauge objects are used to retrieve information about the vacuum system measurement devices and 35 | # the actual pressures measured with them. 36 | gauges = self._tem.Vacuum.Gauges 37 | gauge_names = ['IGPa (accelerator)', 'IGPco (column)', 'PIRco (detector)', 38 | 'PPm (airlock)', 'IGPf (electron gun)'] 39 | 40 | # Build a dictionary with the gauge name, pressure in Pascals, and pressure in log units. 41 | pressure_dictionary = dict() 42 | for i in range(len(gauges)): 43 | pressure_dictionary.update({i: [gauge_names[i], gauges[i].Pressure, pascal_to_log(gauges[i].Pressure)]}) 44 | 45 | return pressure_dictionary 46 | 47 | def print_vacuum_info(self) -> None: 48 | """ 49 | Print out the vacuum info in a table-like format: 50 | Gauge Pressure [Pa] Pressure [Log] 51 | 52 | :return: None. 53 | """ 54 | pressure_info = self._pull_vacuum_info() 55 | 56 | print("{:<25} {:<20} {:<20}".format('Gauge', 'Pressure [Pa]', 'Pressure [Log]')) # Table header 57 | for key, value in pressure_info.items(): 58 | name, pressure_in_pascals, pressure_in_log = value 59 | print("{:<25} {:<20} {:<20}".format(name, '{:0.3e}'.format(pressure_in_pascals), round(pressure_in_log, 3))) 60 | 61 | def get_accelerator_vacuum(self, units: str = "log") -> float: 62 | """ 63 | Obtain the vacuum level up on the outside of the gun. 64 | :param units: string: 65 | The units of the returned accelerator gauge pressure. Either "Pascal" or "log". 66 | :return: float: 67 | The current accelerator gauge pressure, in the requested units. 68 | """ 69 | gauge_name, pressure_in_pascals, pressure_in_log = self._pull_vacuum_info()[0] 70 | units = str(units).lower() 71 | 72 | if units in {"log", "logs"}: 73 | return pressure_in_log 74 | elif units in {"pascal", "pascals"}: 75 | return pressure_in_pascals 76 | else: 77 | print("Error getting accelerator vacuum: units '" + units + "' not recognized.") 78 | return math.nan 79 | 80 | def get_column_vacuum(self, units: str = "log") -> float: 81 | """ 82 | Obtain the vacuum the main section of the microscope with the sample, lenses, etc. 83 | :param units: string: 84 | The units of the returned column gauge pressure. Either "Pascal" or "log". 85 | :return: float: 86 | The current column gauge pressure, in the requested units. 87 | """ 88 | gauge_name, pressure_in_pascals, pressure_in_log = self._pull_vacuum_info()[1] 89 | units = str(units).lower() 90 | 91 | if units in {"log", "logs"}: 92 | return pressure_in_log 93 | elif units in {"pascal", "pascals"}: 94 | return pressure_in_pascals 95 | else: 96 | print("Error getting column vacuum: units '" + units + "' not recognized.") 97 | return math.nan 98 | 99 | def column_under_vacuum(self) -> bool: 100 | """ 101 | This function checks to make sure the column is under vacuum. 102 | 103 | This is a safety check. Since a loss of vacuum can damage the electron gun, there is a valve (column value) 104 | separating the 'accelerator' and 'column' sections of the instrument. If there is a leak, it usually starts at 105 | the column, causing the vacuum pressure to increase first in this section. 106 | 107 | :return: bool: 108 | True: The column is under vacuum (it is therefore safe to open the column valve and take images). 109 | False: The column is not under vacuum (the column valve must remain closed to protect the electron gun). 110 | """ 111 | if 0 < self.get_column_vacuum(units="log") < 20: 112 | # The TEM usually operates with a column vacuum around 15 Log, but anything under 20 Log should be okay. 113 | return True 114 | else: 115 | return False # Unsafe 116 | 117 | def get_column_valve_position(self) -> str: 118 | """ 119 | :return: str: 120 | The current column value position, either "open" or "closed". 121 | The column value should always be closed when the microscope is not in use. 122 | """ 123 | if self._tem.Vacuum.ColumnValvesOpen: 124 | return "open" 125 | else: 126 | return "closed" 127 | 128 | def close_column_valve(self) -> None: 129 | """ 130 | Close the column value. 131 | :return: None. 132 | """ 133 | if self.get_column_valve_position().lower() == "closed": 134 | print("The column value is already closed.. no changes made.") 135 | return 136 | 137 | self._tem.Vacuum.ColumnValvesOpen = False 138 | 139 | def open_column_valve(self) -> None: 140 | """ 141 | Open the column valve (if it is safe to do so). 142 | :return: None 143 | """ 144 | if self.column_under_vacuum(): 145 | # The column is under vacuum, it is safe to open the value up to the accelerator. 146 | self._tem.Vacuum.ColumnValvesOpen = True 147 | 148 | else: 149 | raise Exception("The column value cannot be safely opened because the column isn't under sufficient " 150 | "vacuum. Please check microscope pressures with print_vacuum_info().") 151 | 152 | def print_vacuum_status(self) -> None: 153 | """ 154 | Print out the current vacuum status, along with a 'helpful' description. 155 | :return: None 156 | """ 157 | vacuum_status = self._tem.Vacuum.Status 158 | if vacuum_status == 1: 159 | print("vsUnknown (1): Status of vacuum system is unknown.") 160 | elif vacuum_status == 2: 161 | print("vsOff (2): Vacuum system is off.") 162 | elif vacuum_status == 3: 163 | print("vsCameraAir (3): Camera (only) is aired.") 164 | elif vacuum_status == 4: 165 | print("vsBusy (4): Vacuum system is busy, that is: on its way to ‘Ready’, ‘CameraAir’, etc.") 166 | elif vacuum_status == 5: 167 | print("vsReady (5): Vacuum system is ready.") 168 | elif vacuum_status == 6: 169 | print("vsElse (6): Vacuum is in any other state (gun air, all air etc.), and will not come back to ready " 170 | "without any further action of the user.") 171 | else: 172 | raise Exception("Vacuum Status '" + str(vacuum_status) + "' not recognized.") 173 | 174 | 175 | class VacuumInterface(VacuumMixin): 176 | """ 177 | A microscope interface with only vacuum controls. 178 | """ 179 | 180 | def __init__(self): 181 | try: 182 | self._tem = cc.CreateObject("TEMScripting.Instrument") 183 | except OSError as e: 184 | print("Unable to connect to the microscope.") 185 | raise e 186 | -------------------------------------------------------------------------------- /pyTEM/lib/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/lib/mixins/__init__.py -------------------------------------------------------------------------------- /pyTEM/lib/pascal_to_log.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import math 7 | 8 | 9 | def pascal_to_log(pressure_in_pascals: float) -> float: 10 | """ 11 | Convert pressures from pascals into log units. Log units are defined in such a way that a realistic range of 12 | pressures (for that vacuum element) goes from 0 to 100. The advantage of the log units is simplicity and high 13 | sensitivity for the good vacuum values (where it matters) and low sensitivity for poor vacuum values (where it 14 | doesn't). 15 | 16 | :param pressure_in_pascals: float: 17 | A pressure value, in pascals. 18 | 19 | :return: pressure_in_log_units: float: 20 | The provided pressure value, converted into log units. 21 | """ 22 | # Note: math.log() is a natural logarithm. 23 | # TODO: Ask where this conversion can from, because it doesn't seem to be quite right 24 | return 3.5683 * math.log(pressure_in_pascals) + 53.497 25 | -------------------------------------------------------------------------------- /pyTEM/lib/rgb_to_greyscale.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import numpy as np 7 | 8 | 9 | def rgb_to_greyscale(rgb_image: np.ndarray) -> np.ndarray: 10 | """ 11 | Use the luminosity method (standard matplotlib formula) to perform the conversion from RGB (or RGBA or RGBX) to 12 | unsigned 8-bit greyscale. 13 | 14 | :param rgb_image: 3-dimensional array: 15 | A 2-dimensional RGB image, as a 3-dimensional array. 16 | 17 | :return: 2-dimensional array: 18 | The provided 2-dimensional image, but in greyscale. 19 | """ 20 | # Verify the dimensionality of the provided array. 21 | if rgb_image.ndim != 3: 22 | raise Exception("Error: rgb_to_greyscale() input should be 3-dimensional but it is " 23 | + str(rgb_image.ndim) + ".") 24 | 25 | # Verify we have the expected number of channels. If 4 channels it is RGBA or RGBX in which case the 4th channel 26 | # is ignored in the conversion. 27 | _, _, num_channels = np.shape(rgb_image) 28 | if num_channels not in {3, 4}: 29 | raise Exception("Error: rgb_to_greyscale() input should contain 3 or 4 channels in the last dimension, but " 30 | "there are " + str(num_channels) + ".") 31 | 32 | r, g, b = rgb_image[:, :, 0], rgb_image[:, :, 1], rgb_image[:, :, 2] 33 | return (0.2989 * r + 0.5870 * g + 0.1140 * b).astype(np.uint8) 34 | -------------------------------------------------------------------------------- /pyTEM/lib/stock_mrc_extended_header/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/lib/stock_mrc_extended_header/__init__.py -------------------------------------------------------------------------------- /pyTEM/lib/stock_mrc_extended_header/get_stock_mrc_header.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import os 7 | import numpy as np 8 | BASEDIR = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def get_stock_mrc_extended_header() -> np.ndarray: 12 | """ 13 | Read in and return a "stock" extended header. This extended header will allow MRC files to be returned to 14 | :return: 15 | """ 16 | extended_header_path = os.path.join(BASEDIR, "stock_mrc_extended_header.npy") 17 | extended_header = np.load(str(extended_header_path)) 18 | return extended_header 19 | 20 | 21 | if __name__ == "__main__": 22 | ext_header = get_stock_mrc_extended_header() 23 | print(ext_header) 24 | print(len(ext_header)) 25 | print(type(ext_header)) 26 | print(type(ext_header[0])) 27 | print(ext_header.tobytes()) 28 | -------------------------------------------------------------------------------- /pyTEM/lib/stock_mrc_extended_header/stock_mrc_extended_header.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/lib/stock_mrc_extended_header/stock_mrc_extended_header.npy -------------------------------------------------------------------------------- /pyTEM/lib/tem_tilt_speed.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | 7 | def tem_tilt_speed(speed_deg_per_s: float) -> float: 8 | """ 9 | Convert from tilt speed in degrees per second to the TEM fractional tilt speed required by 10 | Interface.set_stage_position(). 11 | 12 | :param speed_deg_per_s: float: 13 | Tilt speed, in degrees per second. 14 | 15 | :return: float: 16 | The equivalent TEM fractional tilt speed required by Interface.set_stage_position(). 17 | """ 18 | 19 | if speed_deg_per_s > 15: 20 | raise Exception("The microscope's maximum tilt speed is 15 deg / s.") 21 | 22 | elif speed_deg_per_s > 1.5: 23 | # This conversion works well for larger tilting speeds, R^2 = 0.99985445 24 | return 0.000267533 * (speed_deg_per_s ** 3) \ 25 | - 0.002387867 * (speed_deg_per_s ** 2) \ 26 | + 0.043866877 * speed_deg_per_s \ 27 | - 0.004913243 # Notice that the non-zero y-intercepts are likely due to a constant delay 28 | 29 | elif speed_deg_per_s <= 0.000376177 / 0.034841591: 30 | raise Exception("The microscope's minimum tilt speed is 0.02 deg / s.") 31 | 32 | else: 33 | # This conversion works well for low tilt speeds where the relationship is ~linear, R^2 = 0.999982772 34 | return 0.034841591 * speed_deg_per_s \ 35 | - 0.000376177 # Notice that the non-zero y-intercepts are likely due to a constant delay 36 | 37 | -------------------------------------------------------------------------------- /pyTEM/lib/tilt_control.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import math 7 | import time 8 | 9 | from typing import Union 10 | from numpy.typing import ArrayLike 11 | 12 | from pyTEM.lib.mixins.StageMixin import StageInterface 13 | from pyTEM.lib.tem_tilt_speed import tem_tilt_speed 14 | 15 | 16 | def tilt_control(num_acquisitions: int, barriers: ArrayLike, integration_time: float, 17 | tilt_bounds: Union[ArrayLike, None], verbose: bool = False) -> None: 18 | """ 19 | Support pyTEM.Interface.acquisition() with the simultaneous tilt control. For more information on why this 20 | is helpful, please refer to pyTEM.Interface.acquisition(). 21 | 22 | This function is to be run in a parallel thread or process while an acquisition (or acquisition series) is being 23 | performed from the main thread/process. 24 | 25 | :param num_acquisitions: int: 26 | The number of acquisitions to perform. Must be at least 1. 27 | :param barriers: Array of mp.Barrier: 28 | An array of barriers, these are used to synchronize with the main thread/process before each 29 | acquisition. 30 | If this is being run in a parallel thread, this should be an array of threading.Barrier objects or similar. And 31 | if it is being run in a parallel process then this should be an array of multiprocessing.Barrier objects or 32 | similar. Either way, the barriers need to have a party allocated for the process/thread in which this function 33 | is running. 34 | :param integration_time: float: 35 | Exposure time for a single acquisition, in seconds. # TODO: Allow variable integration times 36 | 37 | :param tilt_bounds: float (optional; default is None): 38 | An array of alpha start-stop values for the tilt acquisition(s), in degrees. 39 | len(tilt_bounds) should equal num_acquisitions + 1 40 | Doesn't need to be evenly spaced, the tilt speed will adapt for each individual acquisition. 41 | We will ensure we are at the starting angle (tilt_bounds[0]) before performing the first acquisition. 42 | Upon return the stage is left at the final destination -> tilt_bounds[-1]. 43 | 44 | :param verbose: (optional; default is False): 45 | Print out extra information. Useful for debugging. 46 | 47 | :return: None. 48 | """ 49 | if num_acquisitions <= 0: 50 | raise Exception("Error: tilt_control() requires we perform at least one acquisition.") 51 | 52 | if len(barriers) != num_acquisitions: 53 | raise Exception("Error: tilt_control() didn't receive the expected number of barriers. Expected " 54 | + str(num_acquisitions) + ", but received " + str(len(barriers)) + ".") 55 | 56 | if len(tilt_bounds) != num_acquisitions + 1: 57 | raise Exception("Error: The length of the tilt_bounds array (" + str(len(tilt_bounds)) 58 | + ") received by tilt_control() is inconsistent with the requested number of " 59 | "requested acquisitions (" + str(num_acquisitions) + ").") 60 | 61 | # Build an interface with stage and blanker controls that this process can use to control the microscope. 62 | interface = StageInterface() 63 | 64 | # Ensure we are at the starting tilt angle. 65 | if not math.isclose(interface.get_stage_position_alpha(), tilt_bounds[0], abs_tol=0.01): 66 | if verbose: 67 | print("Moving the stage to the start angle, \u03B1=" + str(tilt_bounds[0])) 68 | interface.set_stage_position_alpha(alpha=tilt_bounds[0], speed=0.25, movement_type="go") 69 | 70 | # Loop through the requested number of acquisitions. 71 | for i in range(num_acquisitions): 72 | 73 | # Compute tilt speed 74 | distance_tilting = abs(tilt_bounds[i + 1] - tilt_bounds[i]) # deg 75 | tilt_speed = distance_tilting / integration_time # deg / s 76 | tilt_speed = tem_tilt_speed(tilt_speed) # convert to fractional speed as required by the stage setters. 77 | 78 | barriers[i].wait() # Synchronize with the main process. 79 | 80 | # Wait while the camera is blind. Notice we wait a little longer than the integration time, this is because 81 | # the acquisition command takes a little longer to issue than the tilt command. 82 | time.sleep(0.03 + integration_time) 83 | 84 | # Perform tilt. This blocks the program for the full integration time so no need to sleep. 85 | tilt_start_time = time.time() 86 | interface.set_stage_position_alpha(alpha=tilt_bounds[i + 1], speed=tilt_speed, movement_type="go") 87 | tilt_stop_time = time.time() 88 | 89 | if verbose: 90 | print("-- Timing results from tilt_control() for acquisition #" + str(i) + " --") 91 | 92 | print("\nStarted titling at: " + str(tilt_start_time)) 93 | print("Stopped tilting at: " + str(tilt_stop_time)) 94 | print("Total time spent tilting: " + str(tilt_stop_time - tilt_start_time)) 95 | -------------------------------------------------------------------------------- /pyTEM/test/StagePosition_testing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Some manual testing of the StagePosition class 4 | """ 5 | 6 | from pyTEM.lib.StagePosition import StagePosition 7 | 8 | 9 | print("\nCreating a stage position object with x=0, y=1, z=2, alpha=-0.35, and beta=0.35...") 10 | stage_position_object = StagePosition(x=0, y=1, z=2, alpha=-0.35, beta=0.35) 11 | print("The stages current x coordinate=" + str(stage_position_object.get_x())) 12 | print("The stages current y coordinate=" + str(stage_position_object.get_y())) 13 | print("The stages current z coordinate=" + str(stage_position_object.get_z())) 14 | print("The stages current alpha coordinate=" + str(stage_position_object.get_alpha())) 15 | print("The stages current beta coordinate=" + str(stage_position_object.get_beta())) 16 | 17 | input("Press Enter to continue...") 18 | 19 | print("\nUpdating the stage location object to x=-1, y=-2, z=-3, alpha=0.7, and beta=-0.7...") 20 | stage_position_object.set_x(new_x=-1) 21 | stage_position_object.set_y(new_y=-2) 22 | stage_position_object.set_z(new_z=-3) 23 | stage_position_object.set_alpha(new_alpha=0.7) 24 | stage_position_object.set_beta(new_beta=-0.7) 25 | print("The stages current x coordinate=" + str(stage_position_object.get_x())) 26 | print("The stages current y coordinate=" + str(stage_position_object.get_y())) 27 | print("The stages current z coordinate=" + str(stage_position_object.get_z())) 28 | print("The stages current alpha coordinate=" + str(stage_position_object.get_alpha())) 29 | print("The stages current beta coordinate=" + str(stage_position_object.get_beta())) 30 | 31 | print("\n###### END OF TESTING ######") 32 | input("Press Enter to exit script...") 33 | -------------------------------------------------------------------------------- /pyTEM/test/TEMInterface_testing_stage_movements.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some manual testing of the pyTEM stage stuff - getting and setting the stage position, etc. 3 | """ 4 | 5 | from pyTEM.Interface import Interface 6 | talos = Interface() 7 | 8 | print("Getting initial stage position...") 9 | stage_position = talos.get_stage_position() 10 | print("The stages current x coordinate=" + str(stage_position.get_x())) 11 | print("The stages current y coordinate=" + str(stage_position.get_y())) 12 | print("The stages current z coordinate=" + str(stage_position.get_z())) 13 | print("The stages current alpha coordinate=" + str(stage_position.get_alpha())) 14 | print("The stages current beta coordinate=" + str(stage_position.get_beta())) 15 | 16 | input("Press Enter to continue...") 17 | 18 | print("\nWe are going to modify the initial stage position object, this should not affect the microscope...") 19 | stage_position.set_x(4) 20 | stage_position.set_alpha(6) 21 | 22 | input("Press Enter to continue...") 23 | 24 | -------------------------------------------------------------------------------- /pyTEM/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/test/__init__.py -------------------------------------------------------------------------------- /pyTEM/test/acquisition_timing/acquisition_timing.txt: -------------------------------------------------------------------------------- 1 | Requested exposure time: 2 s 2 | Core acquisition time: 3 | 4.666405439376831 4 | 4.662489652633667 5 | 4.644346714019775 6 | 4.6553778648376465 7 | 8 | Total acquisition time: 9 | 4.952165126800537 10 | 4.900120973587036 11 | 4.934117317199707 12 | 4.92208456993103 13 | 14 | 15 | 16 | Requested exposure time: 3 s 17 | Core acquisition time: 18 | 6.653700828552246 19 | 6.654743671417236 20 | 21 | Total acquisition time: 22 | 6.9384565353393555 23 | 6.951532363891602 24 | 25 | 26 | 27 | Requested exposure time: 6 s 28 | Core acquisition time: 29 | 12.652272701263428 30 | 31 | 32 | Total acquisition time: 33 | 12.95307183265686 34 | 35 | 36 | 37 | 38 | 39 | Requested exposure time: 0.5 s 40 | Core acquisition time: 41 | 1.6754534244537354 42 | 43 | 44 | Total acquisition time: 45 | 1.9170916080474854 46 | 47 | 48 | 49 | 50 | 51 | Time between overall acquisition start and core start: 52 | 0.03007984161376953 53 | 0.024063825607299805 (3s acq) 54 | 0.02306079864501953 (6s acq) 55 | 0.023056983947753906 (0.5s acq) 56 | 57 | Time between overall acquisition core and overall acquisition end (here is where you build the acq object and such): 58 | 0.2667090892791748 59 | 0.2737274169921875 (3s acq) 60 | 0.2777383327484131 (6s acq) 61 | 0.2185811996459961 (0.5s acq) 62 | 63 | 64 | Conclusions: 65 | Time required to complete the whole acquisition is: 2 * exposure_time + 0.94, most of this extra time is from after the core acquisition 66 | 67 | Time required to complete the core acquisition is: 2 * exposure_time + 0.66 68 | Assume half of this 0.66 hapens before and half after, no way to tell. 69 | 70 | Once we call acquisition(), we should wait (0.025 + 0.33 + exposure_time) before we start tilting. -------------------------------------------------------------------------------- /pyTEM/test/array_size.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def optional_args_func(*args): 5 | print("args: " + str(args)) 6 | print(len(args)) 7 | print(type(args)) 8 | 9 | 10 | my_arr = np.random.random((3, 1024, 1024)) 11 | 12 | print(np.shape(my_arr)) 13 | 14 | print(np.shape(my_arr)[0]) 15 | 16 | for i in range(np.shape(my_arr)[0]): 17 | print() 18 | print(i) 19 | print(np.shape(my_arr[i])) 20 | 21 | print("\n\n-- Testing optional arguments --") 22 | optional_args_func() 23 | optional_args_func(1) 24 | optional_args_func(1, 5, "g") 25 | 26 | 27 | -------------------------------------------------------------------------------- /pyTEM/test/blanker_tilt_control.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import math 7 | import time 8 | 9 | import comtypes.client as cc 10 | 11 | from typing import Union 12 | from numpy.typing import ArrayLike 13 | 14 | from pyTEM.lib.mixins.BeamBlankerMixin import BeamBlankerMixin 15 | from pyTEM.lib.mixins.StageMixin import StageMixin 16 | from pyTEM.lib.tem_tilt_speed import tem_tilt_speed 17 | 18 | 19 | def blanker_tilt_control(num_acquisitions: int, 20 | barriers: ArrayLike, 21 | exposure_time: float, 22 | blanker_optimization: bool, 23 | tilt_bounds: Union[ArrayLike, None], 24 | verbose: bool = False) -> None: 25 | """ 26 | Support pyTEM.Interface.acquisition() with the control of blanking and/or tilting. For more information on why this 27 | is helpful, please refer to pyTEM.Interface.acquisition(). 28 | 29 | This function is to be run in a parallel thread or process while an acquisition (or acquisition series) is being 30 | performed from the main thread/process. 31 | 32 | :param num_acquisitions: int: 33 | The number of acquisitions to perform. Must be at least 1. 34 | :param barriers: Array of mp.Barrier: 35 | An array of multiprocessing barriers, these are used to synchronize with the main thread/process before each 36 | acquisition. 37 | :param exposure_time: float: 38 | Exposure time for a single acquisition, in seconds. If tilting, then this can be thought of more as an 39 | integration time. # TODO: Allow variable exposure times 40 | 41 | :param blanker_optimization: bool: 42 | Whether to perform blanker optimization or not. 43 | :param tilt_bounds: float (optional; default is None): 44 | An array of alpha start-stop values for the tilt acquisition(s), in degrees. 45 | len(tilt_bounds) should equal num_acquisitions + 1 46 | Doesn't need to be evenly spaced, the tilt speed will adapt for each individual acquisition. 47 | We will ensure we are at the starting angle (tilt_bounds[0]) before performing the first acquisition. 48 | Upon return the stage is left at the final destination -> tilt_bounds[-1]. 49 | If None, or an array of length 0, then no tilting is performed. 50 | 51 | :param verbose: (optional; default is False): 52 | Print out extra information. Useful for debugging. 53 | 54 | :return: None. 55 | """ 56 | if num_acquisitions <= 0: 57 | raise Exception("Error: blanker_tilt_control() requires we perform at least one acquisition.") 58 | 59 | if len(barriers) != num_acquisitions: 60 | raise Exception("Error: blanker_tilt_control() didn't receive the expected number of barriers. Expected " 61 | + str(num_acquisitions) + ", but received " + str(len(barriers)) + ".") 62 | 63 | # Build an interface with stage and blanker controls that this process can use to control the microscope. 64 | interface = StageAndBlankerInterface() 65 | 66 | tilting = False # Assume we are not tilting 67 | if tilt_bounds is not None: 68 | # Then we expect an array, either of length 0 or num_acquisitions + 1. 69 | if len(tilt_bounds) == 0: 70 | pass 71 | if len(tilt_bounds) == num_acquisitions + 1: 72 | tilting = True # We need to tilt. 73 | 74 | # Ensure we are at the starting tilt angle. 75 | if not math.isclose(interface.get_stage_position_alpha(), tilt_bounds[0], abs_tol=0.01): 76 | if verbose: 77 | print("Moving the stage to the start angle, \u03B1=" + str(tilt_bounds[0])) 78 | interface.set_stage_position_alpha(alpha=tilt_bounds[0], speed=0.25, movement_type="go") 79 | else: 80 | raise Exception("Error: The length of the non-empty tilt_bounds array (" + str(len(tilt_bounds)) 81 | + ") received by blanker_tilt_control() is inconsistent with the requested number of " 82 | "requested acquisitions (" + str(num_acquisitions) + ").") 83 | 84 | # Declarations for warning suppression 85 | beam_unblank_time, beam_reblank_time, tilt_start_time, tilt_stop_time = None, None, None, None 86 | 87 | # Loop through the requested number of acquisitions. 88 | for i in range(num_acquisitions): 89 | 90 | if tilting: 91 | # Compute tilt speed 92 | distance_tilting = abs(tilt_bounds[i + 1] - tilt_bounds[i]) # deg 93 | tilt_speed = distance_tilting / exposure_time # deg / s 94 | tilt_speed = tem_tilt_speed(tilt_speed) # convert to fractional speed as required by the stage setters. 95 | 96 | barriers[i].wait() # Synchronize with the main process. 97 | 98 | # Wait while the camera is blind. 99 | time.sleep(exposure_time) 100 | 101 | if blanker_optimization: 102 | # Unblank while the acquisition is active. 103 | interface.unblank_beam() 104 | beam_unblank_time = time.time() 105 | 106 | if tilting: 107 | # Perform a tilt, this blocks the program for the full integration time so no need to sleep. 108 | tilt_start_time = time.time() 109 | interface.set_stage_position_alpha(alpha=tilt_bounds[i + 1], speed=tilt_speed, movement_type="go") 110 | tilt_stop_time = time.time() 111 | else: 112 | # Otherwise, we have to wait while the camera is recording. 113 | time.sleep(exposure_time) 114 | 115 | if blanker_optimization: 116 | # Re-blank the beam. 117 | interface.blank_beam() 118 | beam_reblank_time = time.time() 119 | 120 | if verbose: 121 | print("-- Timing results from blanker_tilt_control() for acquisition #" + str(i) + " --") 122 | if blanker_optimization: 123 | print("\nUnblanked the beam at: " + str(beam_unblank_time)) 124 | print("Re-blanked the beam at: " + str(beam_reblank_time)) 125 | print("Total time spent with the beam unblanked: " + str(beam_reblank_time - beam_unblank_time)) 126 | if tilting: 127 | print("\nStarted titling at: " + str(tilt_start_time)) 128 | print("Stopped tilting at: " + str(tilt_stop_time)) 129 | print("Total time spent tilting: " + str(tilt_stop_time - tilt_start_time)) 130 | 131 | 132 | class StageAndBlankerInterface(StageMixin, BeamBlankerMixin): 133 | """ 134 | A microscope interface with only stage and blanker controls. 135 | """ 136 | 137 | def __init__(self): 138 | try: 139 | self._tem = cc.CreateObject("TEMScripting.Instrument") 140 | except OSError as e: 141 | print("Unable to connect to the microscope.") 142 | raise e 143 | -------------------------------------------------------------------------------- /pyTEM/test/cv2_test.py: -------------------------------------------------------------------------------- 1 | # Just some simple testing to get started with openCV 2 | # openCV is a computer vision library that may be required to help solve the tilting problem 3 | import pathlib 4 | import sys 5 | import numpy as np 6 | # import cv2 as cv 7 | # print("Your OpenCV version is: " + cv.__version__) 8 | 9 | in_dir = pathlib.Path(__file__).parent.resolve() 10 | in_file = in_dir / "data" / "Tiltseies_28k_-20-20deg_0.5degps_1.ser" 11 | 12 | 13 | # image = cv.imread(cv.samples.findFile(relative_path="")) 14 | # 15 | # if image is None: 16 | # sys.exit() 17 | -------------------------------------------------------------------------------- /pyTEM/test/epoch_to_datetime.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | epoch_us = 1655112197284907 # Epoch in microseconds 4 | epoch_s = epoch_us / 1000000 # Epoch in seconds 5 | 6 | date_time = datetime.fromtimestamp(epoch_s) 7 | 8 | print(date_time) 9 | 10 | # Extract date and time 11 | print(date_time.date()) 12 | print(date_time.time()) 13 | -------------------------------------------------------------------------------- /pyTEM/test/find_path.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | package_directory = pathlib.Path().resolve().parent.resolve() 3 | 4 | print(package_directory) 5 | -------------------------------------------------------------------------------- /pyTEM/test/flip_and_rotate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test flipping and rotating images with numpy 3 | """ 4 | import pathlib 5 | 6 | import numpy as np 7 | from matplotlib import pyplot as plt 8 | 9 | from pyTEM.lib.Acquisition import Acquisition 10 | 11 | 12 | out_dir = pathlib.Path(__file__).parent.resolve().parent.resolve() \ 13 | / "interface" / "test_images" 14 | out_file = out_dir / "cat.jpeg" 15 | 16 | print("Building acquisition from " + str(out_file)) 17 | acq = Acquisition(out_file) 18 | 19 | img = acq.get_image() 20 | print(np.shape(img)) 21 | print(acq.get_image()) 22 | 23 | # flipped_image = np.flip(img, axis=1) 24 | # rotated_image = np.rot90(flipped_image) 25 | 26 | # All in one step 27 | img = np.rot90(np.flip(img, axis=1)) 28 | img_plot = plt.imshow(img, cmap='Greys_r') 29 | plt.show() 30 | 31 | 32 | -------------------------------------------------------------------------------- /pyTEM/test/hyperspy_test.py: -------------------------------------------------------------------------------- 1 | # Just some simple testing to get started with HyperSpy 2 | # HyperSpy is an open source Python library which provides tools to facilitate the interactive data analysis 3 | 4 | import os 5 | import pathlib 6 | 7 | import matplotlib # import matplotlib for the GUI backend HyperSpy needs 8 | matplotlib.rcParams["backend"] = "Agg" 9 | 10 | import hyperspy.api as hs 11 | 12 | 13 | def turn_off_hyperspy_warnings(): 14 | """ 15 | By default, HyperSpy warns the user if one of the GUI packages is not installed. Turn these warnings off. 16 | :return: None 17 | """ 18 | hs.preferences.GUIs.warn_if_guis_are_missing = False 19 | hs.preferences.save() 20 | 21 | 22 | def convert_images(in_file, out_file_type='tif', index_by_tilt_angle=False): 23 | """ 24 | Read in image(s) from one file format - convert and save them to another 25 | 26 | Often this will be used to convert images from the Thermo Fisher (formerly FEI) .emi/.ser files provided by the 27 | microscope to a standard image format (likely TIFF to maintain 32-bit quality). 28 | 29 | :param in_file: str or Path object: 30 | Path to the file to read in, including the file extension. Must be one of HyperSpy's supported file formats. 31 | :param out_file_type: str (optional; default is 'tif') : 32 | The extension of the out file(s). The image will be converted and saved to this file type. Again, must be one of 33 | HyperSpy's supported file formats. 34 | :param index_by_tilt_angle: bool (optional; default is False): 35 | Index output files by alpha tilt angle. If False, output files sequentially (1, 2, ...). 36 | # TODO: All images in a series may be labelled with the same tilt angle. 37 | 38 | :return: None; images are converted to the out_file_type and saved in out_dir. 39 | """ 40 | 41 | image_stack = hs.load(in_file) # TODO: Investigate lazy loading for large data sets 42 | print("Reading " + str(image_stack) + " from in_file: " + str(in_file)) 43 | 44 | out_file, _ = os.path.splitext(in_file) 45 | nav_dimension = image_stack.axes_manager.navigation_dimension 46 | 47 | if nav_dimension == 0: 48 | # There is only one image 49 | if index_by_tilt_angle: 50 | this_images_out_file = out_file + "_" \ 51 | + str(image_stack.metadata.Acquisition_instrument.TEM.Stage.tilt_alpha) \ 52 | + "." + out_file_type 53 | else: 54 | this_images_out_file = out_file + "." + out_file_type 55 | print("\nSaving image as " + this_images_out_file + "...") 56 | image_stack.save(filename=this_images_out_file, overwrite=True, extension=out_file_type) 57 | 58 | elif nav_dimension == 1: 59 | # We have a stack of images, loop through it 60 | for i, image in enumerate(image_stack): 61 | if index_by_tilt_angle: 62 | index = image.metadata.Acquisition_instrument.TEM.Stage.tilt_alpha 63 | else: 64 | index = i 65 | this_images_out_file = out_file + "_" + str(index) + "." + out_file_type 66 | print("\nSaving image #" + str(i) + " as " + this_images_out_file + "...") 67 | image.save(filename=this_images_out_file, overwrite=True, extension=out_file_type) 68 | 69 | else: 70 | raise Exception("Error: navigation dimension not recognized.") 71 | 72 | 73 | def compute_microscope_shift(template, image): 74 | """ 75 | Compute the microscope shift required to align the provided image with the provided template. 76 | 77 | :param template: str or Path object: 78 | Path to template, including the file extension. The template is the image wherein the particle is already 79 | centered. 80 | 81 | :param image: str or Path object: 82 | Path to the image, the microscope will be adjusted so that this image is centered. 83 | 84 | :return: idk yet 85 | """ 86 | 87 | out_file_image, _ = os.path.splitext(image) # Where to save the shifted image 88 | 89 | print("\nLoading template... ") 90 | template = hs.load(template) 91 | print("Template: " + str(template)) 92 | 93 | print("\nLoading image... ") 94 | image = hs.load(image) 95 | print("Image: " + str(image)) 96 | 97 | # Stack the template and image 98 | print("\nStacking the template and the image... ") 99 | stack = hs.stack(signal_list=[template, image]) 100 | print(stack) 101 | 102 | # Print the shift 103 | print("\nEstimating the shift... ") 104 | shifts = stack.estimate_shift2D() 105 | print("Shifts: " + str(shifts)) 106 | 107 | # TODO: Convert this image shift into a microscope shift (image deflector shift and possibly beam deflector shift) 108 | 109 | # Align the image to the template 110 | print("\nShifting the image to align with the template... ") 111 | stack.align2D(shifts=shifts) 112 | shifted_image = stack.inav[1] 113 | print(shifted_image) 114 | 115 | print("\nSaving the original image as a png... ") 116 | image.save(filename=out_file_image + '.png', overwrite=True, extension='png') 117 | 118 | print("\nSaving the shifted image as a png... ") 119 | # Save the shifted image to file so we can 120 | out_file_image = out_file_image + '_shifted.png' 121 | shifted_image.save(filename=out_file_image, overwrite=True, extension='png') 122 | 123 | 124 | def deck_alignment(): 125 | """ 126 | Test aligning a whole series of images. 127 | Not sure yet if this is a good idea... but I suspect there will be more mechanical error with this approach but 128 | less drift error. 129 | 130 | :return: None 131 | """ 132 | in_dir = pathlib.Path(__file__).parent.resolve() 133 | in_file = in_dir / "data" / "uED_tilt_series_example1.emi" 134 | out_file = in_dir / "data" / "Aligned Stack" / "uED_tilt_series_example1-" 135 | print(out_file) 136 | 137 | image_stack = hs.load(in_file) 138 | sub_stack = image_stack.inav[0:45] # All the other images are just black 139 | print(sub_stack) 140 | 141 | shifts = sub_stack.estimate_shift2D() 142 | 143 | for i, image in enumerate(sub_stack): 144 | print("Shift for image" + str(i) + ": " + str(shifts[i])) 145 | 146 | # Align the image to the template 147 | sub_stack.align2D(shifts=shifts) 148 | 149 | for i, image in enumerate(sub_stack): 150 | this_images_out_file = str(out_file) + str(i) + ".png" 151 | print("\nSaving image #" + str(i) + " as " + this_images_out_file + "...") 152 | image.save(filename=this_images_out_file, overwrite=True, extension='png') 153 | 154 | 155 | if __name__ == "__main__": 156 | 157 | 158 | """ Test convert_images() """ 159 | # in_dir = pathlib.Path(__file__).parent.resolve() 160 | # 161 | # # Note that the EMI file contains the meta-data while the SER file contains the pictures. 162 | # # Either can be provided to the HyperSpy reader, but both need to be in the same directory. 163 | # # Only images 0 -> 45. The rest are blank. 164 | # in_file = in_dir / "data" / "uED_tilt_series_example1.emi" 165 | # 166 | # # This file is too big to be read in by HyperSpy 167 | # # in_file = in_dir / "data" / "uED_tilt_series_example2.emi" 168 | # 169 | # # Try a single image 170 | # # in_file = in_dir / "data" / "uED_tilt_series_example1_1.tif" 171 | # 172 | # # convert_images(in_file, out_file_type='tif', index_by_tilt_angle=True) 173 | # convert_images(in_file, out_file_type='png', index_by_tilt_angle=False) 174 | 175 | 176 | """ Investigate file meta-data """ 177 | # in_file = in_dir / "data" / "uED_tilt_series_example1_1.tif" 178 | # image = hs.load(in_file) 179 | # print(image) 180 | # print(dir(image)) 181 | # print(image.metadata) 182 | # print(image.metadata.Acquisition_instrument.TEM.Stage.tilt_alpha) 183 | 184 | 185 | """ Test compute_microscope_shift() """ 186 | # in_dir = pathlib.Path(__file__).parent.resolve() 187 | # template = in_dir / "data" / "uED_tilt_series_example1_0.tif" 188 | # 189 | # image = in_dir / "data" / "uED_tilt_series_example1_5.tif" 190 | # 191 | # compute_microscope_shift(template=template, image=image) 192 | 193 | 194 | """ Test deck_alignment() """ 195 | deck_alignment() 196 | -------------------------------------------------------------------------------- /pyTEM/test/image_shift_calibration/Image Shift Calibration.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/test/image_shift_calibration/Image Shift Calibration.xlsx -------------------------------------------------------------------------------- /pyTEM/test/image_shift_calibration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/test/image_shift_calibration/__init__.py -------------------------------------------------------------------------------- /pyTEM/test/image_shift_calibration/image_shift_calibration.py: -------------------------------------------------------------------------------- 1 | # Use come test images to either validate the existing shift matrix or obtain the correct one 2 | 3 | import pathlib 4 | 5 | import matplotlib # import matplotlib for the GUI backend HyperSpy needs 6 | 7 | from pyTEM.test.hyperspy_test import convert_images 8 | 9 | matplotlib.rcParams["backend"] = "Agg" 10 | 11 | import hyperspy.api as hs 12 | 13 | 14 | def image_shift_calibration_2(): 15 | """ 16 | Code used when investigating the results of the second image calibration test. 17 | """ 18 | print("Running image_shift_calibration_2()...") 19 | 20 | in_dir = pathlib.Path(__file__).parent.resolve() 21 | print(in_dir) 22 | 23 | # convert_images(in_file=in_dir / "Calibration_Data" / "Image 0, 0, x=0, y=0 (reference image).emd", 24 | # out_file_type='tif') 25 | # 26 | """ Compute Shifts """ 27 | print("\nReading in the template... ") 28 | template = in_dir / "Calibration_Data" / "2" / "Image 0, 0, x=0, y=0 (reference image).emd" 29 | template = hs.load(template) 30 | 31 | print("\nReading in image 1... ") 32 | image1 = in_dir / "Calibration_Data" / "2" / "Image 1, x=-0.2, y=0.2.emd" 33 | convert_images(in_file=image1, out_file_type='tif') 34 | image1 = hs.load(image1) 35 | 36 | print("\nReading in image 2... ") 37 | image2 = in_dir / "Calibration_Data" / "2" / "Image 2, x=0, y=0.2.emd" 38 | convert_images(in_file=image2, out_file_type='tif') 39 | image2 = hs.load(image2) 40 | 41 | print("\nReading in image 3... ") 42 | image3 = in_dir / "Calibration_Data" / "2" / "Image 3, x=0.2, y=0.2.emd" 43 | convert_images(in_file=image3, out_file_type='tif') 44 | image3 = hs.load(image3) 45 | 46 | print("\nReading in image 4... ") 47 | image4 = in_dir / "Calibration_Data" / "2" / "Image 4, x=-0.2, y=0.emd" 48 | convert_images(in_file=image4, out_file_type='tif') 49 | image4 = hs.load(image4) 50 | 51 | print("\nReading in image 5... ") 52 | image5 = in_dir / "Calibration_Data" / "2" / "Image 5, x=0.2, y=0.emd" 53 | convert_images(in_file=image5, out_file_type='tif') 54 | image5 = hs.load(image5) 55 | 56 | print("\nReading in image 6... ") 57 | image6 = in_dir / "Calibration_Data" / "2" / "Image 6, x=-0.2, y=-0.2.emd" 58 | convert_images(in_file=image6, out_file_type='tif') 59 | image6 = hs.load(image6) 60 | 61 | print("\nReading in image 7... ") 62 | image7 = in_dir / "Calibration_Data" / "2" / "Image 7, x=0, y=-0.2.emd" 63 | convert_images(in_file=image7, out_file_type='tif') 64 | image7 = hs.load(image7) 65 | 66 | print("\nReading in image 8... ") 67 | image8 = in_dir / "Calibration_Data" / "2" / "Image 8, x=0.2, y=-0.2.emd" 68 | convert_images(in_file=image8, out_file_type='tif') 69 | image8 = hs.load(image8) 70 | 71 | print("\nCreating the image stack... ") 72 | stack = hs.stack(signal_list=[template, image1, image2, image3, image4, image5, image6, image7, image8]) 73 | 74 | print("\nComputing the image shifts... ") 75 | shifts = stack.estimate_shift2D() 76 | 77 | for i, image in enumerate(stack): 78 | print("Shift for image" + str(i) + ": " + str(shifts[i])) 79 | 80 | # print("\nAligning the images with the template... ") 81 | # stack.align2D(shifts=shifts) 82 | # 83 | # print("\nSaving the aligned images to file... ") 84 | # out_file = in_dir / "Calibration_Data" / "2" / "Aligned_Stack" / "aligned_image-" 85 | # 86 | # for i, image in enumerate(stack): 87 | # this_images_out_file = str(out_file) + str(i) + ".png" 88 | # print("\nSaving image #" + str(i) + " as " + this_images_out_file + "...") 89 | # image.save(filename=this_images_out_file, overwrite=True, extension='png') 90 | 91 | """ Look at individual image shifts""" 92 | # # The image taken with no beam shift applied 93 | # template = in_dir / "Calibration_Data" / "2" / "Image 0, 0, x=0, y=0 (reference image).emd" 94 | # 95 | # # And the image-shifted image, of which we want to compute the magnitude/direction of shift. 96 | # image = in_dir / "Calibration_Data" / "2" / "Image 0, 0, x=0, y=0 (reference image).emd" 97 | # # image = in_dir / "Calibration_Data" / "2" / "Image 1, x=-0.2, y=0.2.emd" 98 | # # image = in_dir / "Calibration_Data" / "2" / "Image 2, x=0, y=0.2.emd" 99 | # # image = in_dir / "Calibration_Data" / "2" / "Image 3, x=0.2, y=0.2.emd" 100 | # # image = in_dir / "Calibration_Data" / "2" / "Image 4, x=-0.2, y=0.emd" 101 | # # image = in_dir / "Calibration_Data" / "2" / "Image 5, x=0.2, y=0.emd" 102 | # # image = in_dir / "Calibration_Data" / "2" / "Image 6, x=-0.2, y=-0.2.emd" 103 | # # image = in_dir / "Calibration_Data" / "2" / "Image 7, x=0, y=-0.2.emd" 104 | # 105 | # compute_microscope_shift(template=template, image=image) 106 | 107 | """ Look at meta-data """ 108 | # emd_file = in_dir / "Calibration_Data" / "2" / "Image 0, 0, x=0, y=0 (reference image).emd" 109 | # emd_image = hs.load(emd_file) 110 | # print(vars(emd_image)) 111 | 112 | 113 | def image_shift_calibration_3(): 114 | """ 115 | Code used when investigating the results of the second image calibration test. 116 | """ 117 | print("Running image_shift_calibration_3()...") 118 | 119 | in_dir = pathlib.Path(__file__).parent.resolve() 120 | print(in_dir) 121 | 122 | """ Look at meta-data """ 123 | # emd_file = in_dir / "Calibration_Data" / "3" / "Image 0, x=0, y=0 (reference image).emd" 124 | # emd_image = hs.load(emd_file) 125 | # print(vars(emd_image)) 126 | 127 | # emd_file = in_dir / "Calibration_Data" / "3" / "Image 2, x=0, y=2.emd" 128 | # emd_image = hs.load(emd_file) 129 | # print(vars(emd_image)) 130 | 131 | """ Compute Shifts """ 132 | print("\nReading in the template... ") 133 | template = in_dir / "Calibration_Data" / "3" / "Image 0, x=0, y=0 (reference image).emd" 134 | convert_images(in_file=template, out_file_type='tif') 135 | template = hs.load(template) 136 | 137 | print("\nReading in image 2... ") 138 | image1 = in_dir / "Calibration_Data" / "3" / "Image 2, x=0, y=2.emd" 139 | convert_images(in_file=image1, out_file_type='tif') 140 | image1 = hs.load(image1) 141 | 142 | print("\nReading in image 4... ") 143 | image2 = in_dir / "Calibration_Data" / "3" / "Image 4, x=-2, y=0.emd" 144 | convert_images(in_file=image2, out_file_type='tif') 145 | image2 = hs.load(image2) 146 | 147 | print("\nReading in image 5... ") 148 | image5 = in_dir / "Calibration_Data" / "3" / "Image 5, x=2, y=0.emd" 149 | convert_images(in_file=image5, out_file_type='tif') 150 | image5 = hs.load(image5) 151 | 152 | print("\nReading in image 7... ") 153 | image7 = in_dir / "Calibration_Data" / "3" / "Image 7, x=0, y=-2.emd" 154 | convert_images(in_file=image7, out_file_type='tif') 155 | image7 = hs.load(image7) 156 | 157 | print("\nCreating the image stack... ") 158 | stack = hs.stack(signal_list=[template, image1, image2, image5, image7]) 159 | 160 | print("\nComputing the image shifts... ") 161 | shifts = stack.estimate_shift2D() 162 | 163 | print("shifts: " + str(type(shifts))) 164 | print("shifts[0]: " + str(type(shifts[0]))) 165 | print("shifts[0][0]: " + str(shifts[0][0])) 166 | 167 | for i, image in enumerate(stack): 168 | print("Shift for image" + str(i) + ": " + str(shifts[i])) 169 | 170 | # print("\nAligning the images with the template... ") 171 | # stack.align2D(shifts=shifts) 172 | # 173 | # print("\nSaving the aligned images to file... ") 174 | # out_file = in_dir / "Calibration_Data" / "3" / "Aligned_Stack" / "aligned_image-" 175 | # 176 | # for i, image in enumerate(stack): 177 | # this_images_out_file = str(out_file) + str(i) + ".png" 178 | # print("\nSaving image #" + str(i) + " as " + this_images_out_file + "...") 179 | # image.save(filename=this_images_out_file, overwrite=True, extension='png') 180 | 181 | """ Look at individual image shifts""" 182 | # # The image taken with no beam shift applied 183 | # template = in_dir / "Calibration_Data" / "3" / "Image 0, x=0, y=0 (reference image).emd" 184 | # convert_images(in_file=template, out_file_type='png') 185 | # 186 | # # And the image-shifted image, of which we want to compute the magnitude/direction of shift. 187 | # # image = in_dir / "Calibration_Data" / "3" / "Image 0, 0, x=0, y=0 (reference image).emd" 188 | # 189 | # image = in_dir / "Calibration_Data" / "3" / "Image 2, x=0, y=2.emd" 190 | # 191 | # # image = in_dir / "Calibration_Data" / "3" / "Image 4, x=-2, y=0.emd" 192 | # # image = in_dir / "Calibration_Data" / "3" / "Image 5, x=2, y=0.emd" 193 | # 194 | # # image = in_dir / "Calibration_Data" / "3" / "Image 7, x=0, y=-2.emd" 195 | # 196 | # compute_microscope_shift(template=template, image=image) 197 | 198 | 199 | if __name__ == "__main__": 200 | """ Compute Image shifts """ 201 | 202 | # image_shift_calibration_2() 203 | image_shift_calibration_3() 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /pyTEM/test/investigate_mrc_files.py: -------------------------------------------------------------------------------- 1 | import mrcfile 2 | import numpy as np 3 | from mrcfile.constants import MAP_ID 4 | import pathlib 5 | import hyperspy.api as hs 6 | 7 | image_dir = pathlib.Path(__file__).parent.resolve() / "test_images" 8 | in_file = image_dir / "Y1908J01-B1 V3 840mm SAD40 2k.mrc" 9 | # in_file = image_dir / "random_image1.mrc" 10 | 11 | mrcfile.validate(in_file) 12 | 13 | with mrcfile.open(in_file, permissive=True) as mrc: 14 | # mrc.header.map = MAP_ID # Fix the header, so we can read without permissive 15 | print("Reading in file: " + str(in_file)) 16 | print(mrc) 17 | print(mrc.header) 18 | print(np.shape(mrc.data)) 19 | 20 | # hs_image = hs.load(in_file) 21 | # print(hs_image) 22 | -------------------------------------------------------------------------------- /pyTEM/test/investigate_test_images.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import pathlib 7 | import hyperspy.api as hs 8 | 9 | in_dir = pathlib.Path(__file__).parent.resolve() / "test_images" 10 | 11 | # Here is an example file where the pixel size registers in imageJ 12 | in_file = in_dir / "Tiltseies_SAD40_-20-20deg_0.5degps_1.1m.tif" 13 | print("Reading in " + str(in_file)) 14 | ref_image = hs.load(in_file) 15 | 16 | print(ref_image) 17 | # print(dir(ref_image)) 18 | print(ref_image.metadata) 19 | # print(ref_image.original_metadata) 20 | 21 | original_metadata = ref_image.original_metadata 22 | print(original_metadata) 23 | print(type(original_metadata)) 24 | print(original_metadata['ResolutionUnit']) 25 | 26 | # And here is an image saved with my save_as_tif() method 27 | in_file = in_dir / "test_image_2.tif" 28 | print("\nReading in " + str(in_file)) 29 | image = hs.load(in_file) 30 | 31 | print(image) 32 | # print(dir(ref_image)) 33 | print(image.metadata) 34 | print(image.original_metadata) 35 | 36 | print(image.original_metadata.ImageDescription) # Metadata dictionary is saved here. 37 | -------------------------------------------------------------------------------- /pyTEM/test/list_indexing.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Camera: 4 | 5 | def __init__(self, name): 6 | self.name = name 7 | 8 | 9 | if __name__ == "__main__": 10 | # desired_camera_name = 'BM-Ceta' 11 | desired_camera_name = 'Fred' 12 | 13 | supported_cameras = [Camera('BM-Ceta'), Camera('BM-Falcon')] 14 | desired_camera_obj = supported_cameras[[c.name for c in supported_cameras].index(desired_camera_name)] 15 | 16 | print(desired_camera_obj.name) 17 | -------------------------------------------------------------------------------- /pyTEM/test/mixins/BigClass.py: -------------------------------------------------------------------------------- 1 | from HelperMixIn1 import HelperMixIn1 2 | from HelperMixIn2 import HelperMixIn2 3 | 4 | 5 | class BigClass(HelperMixIn1, HelperMixIn2): 6 | """ 7 | 8 | """ 9 | 10 | def __init__(self): 11 | self._attribute1 = 5 12 | self._attribute2 = 6 13 | 14 | 15 | if __name__ == "__main__": 16 | """ 17 | """ 18 | 19 | my_big_class = BigClass() 20 | # print(my_big_class.__attribute1) 21 | print(my_big_class.add_attributes()) 22 | print(my_big_class.complicated_expression()) 23 | -------------------------------------------------------------------------------- /pyTEM/test/mixins/HelperMixIn1.py: -------------------------------------------------------------------------------- 1 | # from typing import Union 2 | # 3 | # from Testing_Interface.MixIns.BigClass import BigClass 4 | # from Testing_Interface.MixIns.HelperMixIn2 import HelperMixIn2 5 | 6 | 7 | class HelperMixIn1: 8 | 9 | def add_attributes(self): 10 | return self._attribute1 + self._attribute2 11 | -------------------------------------------------------------------------------- /pyTEM/test/mixins/HelperMixIn2.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | 4 | 5 | class HelperMixIn2: 6 | # attribute1: float 7 | # attribute2: float 8 | 9 | # import BigClass 10 | # import HelperMixIn1 11 | # from Testing_Interface.MixIns.BigClass import BigClass 12 | # from Testing_Interface.MixIns.HelperMixIn1 import HelperMixIn1 13 | 14 | # def complicated_expression(self: Union[BigClass.BigClass, HelperMixIn1.HelperMixIn1, 'HelperMixIn2']): 15 | # return self.attribute1 + self.add_attributes() 16 | def complicated_expression(self): 17 | return self._attribute1 + self.add_attributes() 18 | 19 | -------------------------------------------------------------------------------- /pyTEM/test/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM/test/mixins/__init__.py -------------------------------------------------------------------------------- /pyTEM/test/mrcfile_header_testing.py: -------------------------------------------------------------------------------- 1 | """ 2 | The standard is described here: https://www.ccpem.ac.uk/mrc_format/mrc2014.php. 3 | Details of the MRC file format used by IMOD can be found here: https://bio3d.colorado.edu/imod/doc/mrc_format.txt 4 | Check here for dtypes: https://github.com/ccpem/mrcfile/blob/23ca116b57a72cbef02f39c88c315cad471ad503/mrcfile/dtypes.py 5 | """ 6 | 7 | 8 | # Let's see if it is something hyperspy can load 9 | import pathlib 10 | import numpy as np 11 | import mrcfile 12 | # np.set_printoptions(threshold=np.inf) 13 | 14 | out_dir = pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve() / "test" \ 15 | / "interface" / "test_images" 16 | in_file_ = out_dir / "size_testing.mrc" 17 | # in_file_ = out_dir / "Valid_tilt_series.mrc" 18 | 19 | # mrc_file = mrcfile.validate(in_file_) 20 | # print(mrc_file) 21 | 22 | print("Loading data from " + str(in_file_)) 23 | with mrcfile.open(in_file_, permissive=True) as mrc: 24 | images = mrc.data 25 | print(dir(mrc)) 26 | header = mrc.header 27 | extended_header = mrc.extended_header 28 | 29 | print("\nHeader: ") 30 | print(header) 31 | mrc.print_header() 32 | print(type(header)) 33 | 34 | print("\nExtended Header: ") 35 | print(extended_header) 36 | print(len(extended_header)) 37 | print(type(extended_header)) 38 | 39 | tp = np.dtype("S" + str(mrc.header.nsymbt)) 40 | recovered = np.frombuffer(extended_header, dtype=tp)[0] 41 | 42 | print("\nNumber of Columns (Number of pixels along x): " + str(header.nx)) 43 | print("Number of Rows (Number of pixels along y): " + str(header.ny)) 44 | print("Number of Sections (number of images): " + str(header.nz)) 45 | print("Map: " + str(header.map)) 46 | print("Map testing: " + str(header.getfield(np.dtype('S4'), offset=208))) 47 | print("Number of bytes in the header: " + str(header.nsymbt)) 48 | 49 | # Type of extended header, includes 'SERI' for SerialEM, 'FEI1' for FEI, 'AGAR' for Agard 50 | print("Type of the extended header: " + str(header.getfield(np.dtype('S4'), offset=104))) 51 | 52 | print("RMS deviation of densities from mean: " + str(header.getfield(np.dtype('f4'), offset=216))) 53 | 54 | np.save(str(out_dir / "stock_mrc_extended_header"), extended_header) 55 | 56 | 57 | # in_file_2 = out_dir / "size_testing.mrc" 58 | # print("Loading extended header into " + str(in_file_2)) 59 | # with mrcfile.open(in_file_2, permissive=True, mode='r+') as mrc: 60 | # mrc.set_extended_header(extended_header) 61 | # print("New extended header size: " + str(mrc.header.nsymbt)) 62 | 63 | # print(images) 64 | # print(type(images)) 65 | # print(np.shape(images)) 66 | # 67 | # first_image = images[0] 68 | # print(np.shape(first_image)) 69 | # print(type(first_image[0][0])) # Element type 70 | -------------------------------------------------------------------------------- /pyTEM/test/multitasking_testing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import threading 7 | import time 8 | 9 | import numpy as np 10 | from pathos.helpers import mp 11 | 12 | from pyTEM.Interface import Interface 13 | from pyTEM.lib.AcquisitionSeries import AcquisitionSeries 14 | from pyTEM.lib.mixins.BeamBlankerMixin import BeamBlankerInterface 15 | from pyTEM_scripts.test.micro_ed.AcquisitionSeriesProperties import AcquisitionSeriesProperties 16 | 17 | # https://stackoverflow.com/questions/3246525/why-cant-i-create-a-com-object-in-a-new-thread-in-python 18 | 19 | 20 | def multitasking_testing(microscope: Interface, acquisition_properties: AcquisitionSeriesProperties, 21 | shifts: np.array) -> None: 22 | """ 23 | Test to see which microscope systems can be controlled concurrently. 24 | """ 25 | image_stack = AcquisitionSeries() 26 | 27 | print("Tilting to the start position.") 28 | microscope.set_stage_position(alpha=acquisition_properties.alpha_arr[0]) # Move to the start angle 29 | 30 | # Fill in the image stack array 31 | for i, alpha in enumerate(acquisition_properties.alphas): 32 | # So that tilting can happen simultaneously, it will be performed in a separate thread 33 | # tilting_thread = TiltingThread(microscope=microscope, destination=acquisition_properties.alpha_arr[i + 1], 34 | # speed=acquisition_properties.tilt_speed, verbose=verbose) 35 | blanking_process = mp.Process(target=blanker, args=(acquisition_properties.alpha_arr[i + 1], 36 | acquisition_properties.tilt_speed)) 37 | 38 | # Apply the appropriate shift 39 | microscope.set_image_shift(x=shifts[i][0], y=shifts[i][1]) 40 | 41 | # Start tilting 42 | # tilting_thread.start() 43 | blanking_process.start() 44 | 45 | time.sleep(3) 46 | 47 | # Take an image 48 | acq, (core_acq_start, core_acq_end) = microscope.acquisition( 49 | camera_name=acquisition_properties.camera_name, sampling=acquisition_properties.sampling, 50 | exposure_time=acquisition_properties.integration_time) 51 | 52 | # tilting_thread.join() 53 | blanking_process.join() 54 | 55 | print("\nCore acquisition started at: " + str(core_acq_start)) 56 | print("Core acquisition returned at: " + str(core_acq_end)) 57 | print("Core acquisition time: " + str(core_acq_end - core_acq_start)) 58 | 59 | image_stack.append(acq) 60 | 61 | # if verbose: 62 | # print("Saving image stack to file as: " + acquisition_properties.out_file) 63 | # image_stack.save_as_mrc(acquisition_properties.out_file) 64 | 65 | 66 | class TiltingThread(threading.Thread): 67 | 68 | def __init__(self, microscope: Interface, destination: float, speed: float, verbose: bool = False): 69 | """ 70 | :param microscope: pyTEM.Interface: 71 | A pyTEM interface to the microscope. 72 | :param destination: float: 73 | The alpha angle to which we will tilt. 74 | :param speed: float: 75 | Tilt speed, in TEM fractional tilt speed units. 76 | :param verbose: bool: 77 | Print out extra information. Useful for debugging. 78 | """ 79 | 80 | threading.Thread.__init__(self) 81 | self.microscope = microscope 82 | self.destination = destination 83 | self.speed = speed 84 | self.verbose = verbose 85 | 86 | def run(self): 87 | """ 88 | Creating a new COM interface doesn't work. 89 | """ 90 | # pythoncom.CoInitialize() 91 | # tem = cc.CreateObject("TEMScripting.Instrument") 92 | # print(tem.Projection.Mode) 93 | # pythoncom.CoUninitialize() 94 | 95 | """ 96 | Accessing the illumination and projection functionality of the passed interface doesn't work 97 | """ 98 | # print(self.microscope._tem.Stage.Status) 99 | # print(self.microscope._tem.Vacuum.ColumnValvesOpen) 100 | # print(self.microscope._tem.Illumination.BeamBlanked) 101 | 102 | """ 103 | We are able to access the stage controls (including tilting) just fine. 104 | """ 105 | start_time = time.time() 106 | self.microscope.set_stage_position_alpha(alpha=self.destination, speed=self.speed, movement_type="go") 107 | stop_time = time.time() 108 | 109 | print("\nStarting tilting at: " + str(start_time)) 110 | print("Stopping tilting at: " + str(stop_time)) 111 | print("Total time spent tilting: " + str(stop_time - start_time)) 112 | 113 | 114 | class TiltingProcess(mp.Process): 115 | 116 | def __init__(self, microscope: Interface, destination: float, speed: float, verbose: bool = False): 117 | """ 118 | :param microscope: pyTEM.Interface: 119 | A pyTEM interface to the microscope. 120 | :param destination: float: 121 | The alpha angle to which we will tilt. 122 | :param speed: float: 123 | Tilt speed, in TEM fractional tilt speed units. 124 | :param verbose: bool: 125 | Print out extra information. Useful for debugging. 126 | """ 127 | 128 | mp.Process.__init__(self) 129 | # self.microscope = microscope 130 | self.microscope = microscope 131 | self.destination = destination 132 | self.speed = speed 133 | self.verbose = verbose 134 | 135 | def run(self): 136 | # pythoncom.CoInitialize() 137 | # tem = cc.CreateObject("TEMScripting.Instrument") 138 | # pythoncom.CoUninitialize() 139 | # print(self.microscope._tem.Stage.Status) 140 | # print(self.microscope._tem.Vacuum.ColumnValvesOpen) 141 | # print(tem.Projection.Mode) 142 | # print(self.microscope._tem.Illumination.BeamBlanked) 143 | 144 | start_time = time.time() 145 | self.microscope.set_stage_position_alpha(alpha=self.destination, speed=self.speed, movement_type="go") 146 | stop_time = time.time() 147 | 148 | if self.verbose: 149 | print("\nStarting tilting at: " + str(start_time)) 150 | print("Stopping tilting at: " + str(stop_time)) 151 | print("Total time spent tilting: " + str(stop_time - start_time)) 152 | 153 | 154 | def blanker(exposure_time): 155 | """ 156 | 157 | :param exposure_time: float: 158 | The requested exposure time, in seconds. 159 | :return: 160 | """ 161 | beam_blanker_interface = BeamBlankerInterface() 162 | 163 | # tem = cc.CreateObject("TEMScripting.Instrument") 164 | # tem.Illumination.BeamBlanked = True 165 | 166 | # Wait for one exposure_time period while the camera prepares itself. 167 | time.sleep(exposure_time) 168 | 169 | # Unblank for one exposure_time while the acquisition is active. 170 | beam_unblank_time = time.time() 171 | beam_blanker_interface.unblank_beam() 172 | time.sleep(exposure_time) 173 | beam_reblank_time = time.time() 174 | beam_blanker_interface.blank_beam() 175 | 176 | print("\nUnblanked the beam at: " + str(beam_unblank_time)) 177 | print("Re-blanked the beam at: " + str(beam_reblank_time)) 178 | print("Total time spent with the beam unblanked: " + str(beam_reblank_time - beam_unblank_time)) 179 | 180 | 181 | if __name__ == "__main__": 182 | 183 | try: 184 | scope = Interface() 185 | except BaseException as e: 186 | print(e) 187 | print("Unable to connect to microscope, proceeding with None object.") 188 | scope = None 189 | 190 | # BeamBlanker() 191 | 192 | start_alpha_ = -2 193 | stop_alpha_ = 2 194 | step_alpha_ = 1 195 | 196 | num_alpha = int((stop_alpha_ - start_alpha_) / step_alpha_ + 1) 197 | alpha_arr = np.linspace(start=start_alpha_, stop=stop_alpha_, num=num_alpha, endpoint=True) 198 | 199 | aqc_props = AcquisitionSeriesProperties(camera_name='BM-Ceta', alpha_arr=alpha_arr, integration_time=3, 200 | sampling='1k') 201 | 202 | shifts_ = np.full(shape=len(aqc_props.alphas), dtype=(float, 2), fill_value=0.0) 203 | 204 | print("Shifts:") 205 | print(shifts_) 206 | 207 | multitasking_testing(microscope=scope, acquisition_properties=aqc_props, shifts=shifts_) 208 | -------------------------------------------------------------------------------- /pyTEM/test/readme_example_code.py: -------------------------------------------------------------------------------- 1 | from pyTEM.Interface import Interface 2 | 3 | from pathlib import Path 4 | 5 | from pyTEM.lib.mixins.BeamBlankerMixin import BeamBlankerMixin 6 | 7 | my_microscope = Interface() 8 | 9 | """ Acquisition Controls """ 10 | # Get a list of available cameras. 11 | available_cameras = my_microscope.get_available_cameras() 12 | 13 | if len(available_cameras) > 0: 14 | # Let's see what each camera can do... 15 | for camera in available_cameras: 16 | my_microscope.print_camera_capabilities(camera_name=camera) 17 | 18 | # Perform a single acquisition using the first available camera. 19 | acq = my_microscope.acquisition(camera_name=available_cameras[0], exposure_time=1, sampling='4k', 20 | blanker_optimization=True) 21 | 22 | # Display a pop-up with the results of our acquisition. 23 | acq.show_image() 24 | 25 | # Downsample the acquisition (bilinear decimation by a factor of 2). 26 | acq.downsample() 27 | 28 | # Save the acquisition to file. 29 | downloads_path = str(Path.home() / "Downloads") 30 | acq.save_to_file(out_file=downloads_path + "/test_acq.tif") 31 | 32 | else: 33 | print("No available cameras!") 34 | 35 | """ Magnification Controls """ 36 | from pyTEM.Interface import Interface 37 | my_microscope = Interface() 38 | 39 | # Make sure we are in TEM imaging mode. 40 | my_microscope.set_mode(new_mode="TEM") 41 | my_microscope.set_projection_mode(new_projection_mode="imaging") 42 | 43 | # Print out the current magnification. 44 | current_magnification = my_microscope.get_magnification() 45 | print("Current magnification: " + str(current_magnification) + "x Zoom") 46 | 47 | # Print a list of available magnifications. 48 | my_microscope.print_available_magnifications() 49 | 50 | # TEM magnification is set by index, lets increase the magnification by three notches. 51 | current_magnification_index = my_microscope.get_magnification_index() 52 | my_microscope.set_tem_magnification(new_magnification_index=current_magnification_index + 3) 53 | 54 | # And decrease it back down by one notch. 55 | my_microscope.shift_tem_magnification(magnification_shift=-1) 56 | 57 | """ Image and Beam Shift Controls """ 58 | # Print out the current image shift. 59 | u = my_microscope.get_image_shift() 60 | print("Current image shift in the x-direction: " + str(u[0])) 61 | print("Current image shift in the y-axis: " + str(u[1])) 62 | 63 | # Print out the current beam shift. 64 | v = my_microscope.get_beam_shift() 65 | print("\nCurrent beam shift in the x-direction: " + str(v[0])) 66 | print("Current beam shift in the y-direction: " + str(v[1])) 67 | 68 | # Shift the image 2 microns to the right, and 3 microns up. 69 | my_microscope.set_image_shift(x=u[0] + 2, y=u[1] + 3) 70 | 71 | # Move the beam shift to (-10, 5). 72 | my_microscope.set_beam_shift(x=-10, y=5) 73 | 74 | # Print out the new image shift. 75 | u = my_microscope.get_image_shift() 76 | print("\nNew image shift in the x-direction: " + str(u[0])) 77 | print("New image shift in the y-direction: " + str(u[1])) 78 | 79 | # Print out the new beam shift. 80 | v = my_microscope.get_beam_shift() 81 | print("\nNew beam shift in the x-direction: " + str(v[0])) 82 | print("New beam shift in the y-direction: " + str(v[1])) 83 | 84 | # Zero both image and beam shift. 85 | my_microscope.zero_shifts() 86 | 87 | """ Mode Controls """ 88 | # Make sure we are in TEM imaging mode. 89 | my_microscope.set_mode(new_mode="TEM") 90 | my_microscope.set_projection_mode(new_projection_mode="imaging") 91 | 92 | # Print out the current projection submode. 93 | my_microscope.print_projection_submode() 94 | 95 | # Print out the current illumination mode. 96 | print("Current illumination mode: " + my_microscope.get_illumination_mode()) 97 | if my_microscope.get_illumination_mode() == "microprobe": 98 | print("This mode provides a nearly parallel illumination at the cost of a larger probe size.") 99 | else: 100 | print("Use this mode to get a small convergent electron beam.") 101 | 102 | # Switch to STEM mode. 103 | my_microscope.set_mode(new_mode="STEM") 104 | 105 | """ Screen controls """ 106 | if my_microscope.get_screen_position() == "retracted": 107 | my_microscope.insert_screen() 108 | 109 | """ Beam Blanker Controls """ 110 | 111 | if my_microscope.beam_is_blank(): 112 | print("The beam is blank... un-blanking beam...") 113 | my_microscope.unblank_beam() 114 | else: 115 | print("The beam is un-blanked... blanking beam...") 116 | my_microscope.blank_beam() 117 | 118 | 119 | """ Stage Controls """ 120 | 121 | # Print out the stage's current status. 122 | my_microscope.print_stage_status() 123 | 124 | # Reset the microscope stage to the home position. 125 | if my_microscope.stage_is_home(): 126 | pass 127 | else: 128 | my_microscope.reset_stage_position() 129 | 130 | # Update the x, y, and alpha stage positions. Move at half speed. 131 | my_microscope.set_stage_position(x=5, y=10, alpha=-45, speed=0.5) 132 | 133 | # Print out the current stage position. 134 | my_microscope.print_stage_position() 135 | 136 | """ Vacuum Controls """ 137 | 138 | # Print out the current status of the vacuum system. 139 | my_microscope.print_vacuum_status() 140 | 141 | # Print out vacuum info in a table-like format. 142 | my_microscope.print_vacuum_info() 143 | 144 | # Check if the column is under vacuum. 145 | if my_microscope.column_under_vacuum(): 146 | print("The column is currently under vacuum; it is safe to open the column valve.") 147 | else: 148 | print("Column vacuum is insufficient to open the column valve.") 149 | 150 | # Make sure the column valve is closed. 151 | if my_microscope.get_column_valve_position() == "open": 152 | my_microscope.close_column_valve() 153 | 154 | # Create a pyTEM interface with only stage controls. 155 | # Through this interface, we have access to all pyTEM stage-related functions, 156 | # but none of the other functions. 157 | from pyTEM.lib.mixins.StageMixin import StageInterface 158 | 159 | stage_interface = StageInterface() 160 | 161 | # Using a stage interface, we can perform the same series of commands performed earlier 162 | # with a complete interface. 163 | 164 | # Print out the stage's current status. 165 | stage_interface.print_stage_status() 166 | 167 | # Reset the microscope stage to the home position. 168 | if stage_interface.stage_is_home(): 169 | pass 170 | else: 171 | stage_interface.reset_stage_position() 172 | 173 | # Update the x, y, and alpha stage positions. Move at half speed. 174 | stage_interface.set_stage_position(x=5, y=10, alpha=-45, speed=0.5) 175 | 176 | # Print out the current stage position. 177 | stage_interface.print_stage_position() 178 | 179 | import comtypes.client as cc 180 | 181 | from pyTEM.lib.mixins.StageMixin import StageMixin 182 | from pyTEM.lib.mixins.BeamBlankerMixin import BeamBlankerMixin 183 | 184 | 185 | class StageBeamBlankerInterface(StageMixin, BeamBlankerMixin): 186 | """ 187 | A microscope interface with only stage and beam blanker controls. 188 | """ 189 | 190 | def __init__(self): 191 | try: 192 | self._tem = cc.CreateObject("TEMScripting.Instrument") 193 | except OSError as e: 194 | print("Unable to connect to the microscope.") 195 | raise e 196 | 197 | 198 | -------------------------------------------------------------------------------- /pyTEM/test/serialem_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | SerialEM is a program that can acquire a variety of data from electron microscopes: 3 | - tilt series for electron tomography, 4 | - large image areas for 3-D reconstruction from serial sections, 5 | - and images for reconstruction of macromolecules by single-particle methods. 6 | 7 | As shown here: 8 | https://sphinx-emdocs.readthedocs.io/en/latest/serialem-note-hidden-goodies.html#example-5-scripting-with-python, it is 9 | possible to call serialEM functions from Python. SerialEM has lots of interesting high-level commands that could prove 10 | useful in a variety of context -> a list of available serialEM commands can be found here: 11 | https://bio3d.colorado.edu/SerialEM/hlp/html/script_commands.htm 12 | 13 | 14 | In oder to enable remote python, I added the following lines to the SerialEM property file 15 | (C:/ProgramData/SerialEM/SerialEMproperties.txt on the Talos PC): 16 | PathToPython 3.6 C:\FEI\Python36 17 | PythonModulePath C:\Program Files\SerialEM\PythonModules 18 | ScriptMonospaceFont Consolas 19 | EnableExternalPython 1 20 | """ 21 | 22 | import serialem as sem 23 | 24 | print("\nHere is a list of the available serialEM commands:") 25 | print(dir(sem)) 26 | 27 | try: 28 | 29 | print("\nTesting moving the stage..") 30 | sem.MoveStageTo(1, 1) # MoveTo takes input in um 31 | 32 | print("\nTesting obtaining the spot size..") 33 | spot_size = sem.ReportSpotSize 34 | print(type(spot_size)) 35 | print(dir(spot_size)) 36 | 37 | # print("\nPerforming auto alignment..") 38 | # sem.AutoAlign() 39 | 40 | except sem.SEMmoduleError as e: 41 | print("Unable to connect to microscope, please double check that the SerialEM client is running.") 42 | raise e 43 | -------------------------------------------------------------------------------- /pyTEM/test/synchronizing_threads.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | import multiprocessing as mp 5 | 6 | # from pathos.helpers import mp 7 | # from multiprocessing.managers import BaseManager 8 | 9 | 10 | class MyThread(threading.Thread): 11 | def __init__(self, thread_id, lock, name, counter): 12 | threading.Thread.__init__(self) 13 | self.thread_id = thread_id 14 | self.lock = lock 15 | self.name = name 16 | self.counter = counter 17 | 18 | def run(self): 19 | print("Starting " + str(self.name)) 20 | # Get lock to synchronize threads 21 | self.lock.acquire() 22 | print_time(self.name, self.counter, 3) 23 | # Free lock to release next thread 24 | self.lock.release() 25 | 26 | 27 | class MyProcess(mp.Process): 28 | def __init__(self, process_id, barrier, name, counter): 29 | mp.Process.__init__(self) 30 | self.process_id = process_id 31 | self.barrier = barrier 32 | self.name = name 33 | self.counter = counter 34 | self.end_time = None 35 | 36 | def run(self): 37 | print("Starting " + str(self.name)) 38 | 39 | # with self.lock: 40 | # MyManager.register('get_barrier') 41 | # manager = MyManager(address=('localhost', 5555), authkey=b'akey') 42 | # manager.connect() 43 | # barrier = manager.get_barrier() 44 | # print("Got the barrier from the manager") 45 | 46 | # Get lock to synchronize threads 47 | # threadLock.acquire() 48 | print_time(self.name, self.counter, 3) 49 | # Free lock to release next thread 50 | # threadLock.release() 51 | print("Waiting at the barrier...") 52 | self.barrier.wait() 53 | print(time.time()) 54 | 55 | def join(self, *args) -> str: 56 | mp.Process.join(self, *args) 57 | return self.name 58 | 59 | 60 | class ProcessWithBarrier(mp.Process): 61 | def __init__(self, process_id, barrier, barrier2, name): 62 | mp.Process.__init__(self) 63 | self.process_id = process_id 64 | self.barrier = barrier 65 | self.barrier2 = barrier2 66 | self.name = name 67 | 68 | def run(self): 69 | print("Starting " + str(self.name)) 70 | time.sleep(3) 71 | self.barrier.wait() # Note: We should use locks here to prevent stuff from printing at exactly the same time. 72 | print(str(self.name) + " moving on from barrier 1 at " + str(time.time())) 73 | 74 | self.barrier2.wait() 75 | print(str(self.name) + " moving on from barrier 2 at " + str(time.time())) 76 | 77 | def join(self, *args) -> str: 78 | mp.Process.join(self, *args) 79 | return self.name 80 | 81 | 82 | def print_time(thread_name, delay, counter): 83 | while counter: 84 | time.sleep(delay) 85 | print(str(thread_name) + " " + str(time.ctime(time.time()))) 86 | counter -= 1 87 | 88 | 89 | if __name__ == "__main__": 90 | 91 | """ Thread testing """ 92 | # print("Thread testing...") 93 | # threadLock = threading.Lock() 94 | # threads = [] 95 | # 96 | # # Create threads 97 | # thread1 = MyThread(1, "Thread-1", 1) 98 | # thread2 = MyThread(2, "Thread-2", 2) 99 | # 100 | # # Start new Threads 101 | # thread1.start() 102 | # thread2.start() 103 | # 104 | # # Add threads to thread list 105 | # threads.append(thread1) 106 | # threads.append(thread2) 107 | # 108 | # # Wait for all threads to complete 109 | # for t in threads: 110 | # t.join() 111 | 112 | 113 | """ Process testing """ 114 | # print("Process testing...") 115 | # 116 | # barrier_ = mp.Barrier(2) 117 | # 118 | # # Create processes 119 | # process1 = MyProcess(1, barrier_, "Process-1", 1) 120 | # process2 = MyProcess(2, barrier_, "Process-2", 2) 121 | # 122 | # # Start the new processes 123 | # process1.start() 124 | # # print("Process 1 is alive: " + str(process1.is_alive())) 125 | # process2.start() 126 | # 127 | # # Add processes to process list 128 | # processes = [process1, process2] 129 | # 130 | # # Wait for all threads to complete 131 | # for p in processes: 132 | # print(str(p.join()) + " joined at " + str(time.time())) 133 | 134 | 135 | """ Barrier testing """ 136 | print("Barrier testing...") 137 | lock_ = mp.Lock() 138 | 139 | barrier_ = mp.Barrier(2) 140 | barrier2_ = mp.Barrier(2) 141 | # MyManager.register('get_barrier', callable=lambda: barrier) 142 | # manager_ = MyManager(address=('localhost', 5555), authkey=b'akey') 143 | # manager_.start() 144 | 145 | # Create processes 146 | process1 = ProcessWithBarrier(1, barrier_, barrier2_, "Process-1") 147 | 148 | # Start the new processes 149 | process1.start() 150 | 151 | barrier_.wait() # Wait for the process to start 152 | print("Main moving on from barrier 1 at " + str(time.time())) 153 | 154 | time.sleep(3) 155 | barrier2_.wait() 156 | print("Main moving on from barrier 2 at " + str(time.time())) 157 | 158 | # Wait for all threads to complete 159 | print(str(process1.join()) + " joined at " + str(time.time())) 160 | 161 | # We are now all done 162 | print("\nExiting Main Thread") 163 | -------------------------------------------------------------------------------- /pyTEM/test/tif_stack_testing.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import numpy as np 4 | import hyperspy.api as hs 5 | 6 | from tifffile import tifffile 7 | 8 | 9 | out_dir = pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve() / "test" \ 10 | / "interface" / "test_images" 11 | print(out_dir) 12 | in_file_ = out_dir / "example_tiff_stack.tif" 13 | # in_file_ = out_dir / "cat.jpeg" 14 | 15 | print("\nLoading with tifffile...") 16 | input_ = tifffile.imread(str(in_file_)) 17 | print(type(input_)) 18 | print(np.shape(input_)) 19 | print(type(input_[0][0][0])) 20 | 21 | print("\nLoading with Hyperspy...") 22 | input_2 = hs.load(str(in_file_)) 23 | print(type(input_)) 24 | print(np.shape(input_)) 25 | print(type(input_[0][0][0])) 26 | 27 | # out_file = out_dir / "tif_stack_written.tif" 28 | # tifffile.imwrite(out_file, input_) 29 | -------------------------------------------------------------------------------- /pyTEM/test/timing_timing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Check to see how long the time.time() command takes. 3 | 4 | Result: somewhere on the order of e-7 seconds. 5 | """ 6 | 7 | import time 8 | 9 | iterations = 10000000 10 | start_time = time.time() 11 | for i in range(iterations): 12 | dummy = time.time() 13 | 14 | end_time = time.time() 15 | 16 | print((end_time - start_time) / iterations) 17 | -------------------------------------------------------------------------------- /pyTEM_scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/__init__.py -------------------------------------------------------------------------------- /pyTEM_scripts/align_images.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import argparse 7 | 8 | from typing import Tuple 9 | 10 | from pyTEM.lib.Acquisition import Acquisition 11 | from pyTEM.lib.AcquisitionSeries import AcquisitionSeries 12 | from pyTEM_scripts.lib.align_images.GetInOutFile import GetInOutFile 13 | from pyTEM_scripts.lib.align_images.display_goodbye_message import display_goodbye_message 14 | 15 | DESCRIPTION = "Read in some images (possibly from an image stack, possibly from a bunch of single image files), " \ 16 | "align the images, and save the results back to file." \ 17 | "\n\n" \ 18 | "Image shifts are estimated using Hyperspy's estimate_shift2D() function. This function uses a phase " \ 19 | "correlation algorithm based on the following paper: " \ 20 | "\n Schaffer, Bernhard, Werner Grogger, and Gerald Kothleitner. “Automated Spatial Drift Correction " \ 21 | "\n for EFTEMmImage Series.” Ultramicroscopy 102, no. 1 (December 2004): 27–36." 22 | 23 | 24 | def align_images(verbose: bool = False): 25 | """ 26 | Read in some images (possibly from a stack, possibly from a bunch of single image files), align the images, and 27 | save the results back to file. For more information, please refer to the above description, or view using: 28 | 29 | align_images --help 30 | 31 | :param verbose: bool: 32 | Print out extra information. Useful for debugging. 33 | 34 | :return: None. The resulting image stack is saved to file at the location requested by the user. 35 | """ 36 | # Get the in and out file info from the user. 37 | if verbose: 38 | print("Prompting the user for in/out file path information...") 39 | in_file, out_file = GetInOutFile().run() 40 | 41 | if isinstance(in_file, str): 42 | # Then we have a single file, assume it is an image stack. 43 | if verbose: 44 | print("Reading in image stack...") 45 | acq_series = AcquisitionSeries(source=in_file) 46 | 47 | elif isinstance(in_file, Tuple): 48 | # We have a bunch of single images in separate files. 49 | num_in_files = len(in_file) 50 | if verbose: 51 | print("Reading in single image files...") 52 | 53 | acq_series = AcquisitionSeries() 54 | for i, file in enumerate(in_file): 55 | if verbose: 56 | print(" Loading image " + str(i + 1) + " out of " + str(num_in_files) + "...") 57 | acq_series.append(Acquisition(file)) 58 | 59 | else: 60 | raise Exception("Error: in_file type not recognized: " + str(type(in_file))) 61 | 62 | # Go ahead and perform the alignment using AcquisitionSeries' align() method. 63 | if verbose: 64 | print("Performing image alignment...") 65 | acq_series = acq_series.align() 66 | if verbose: 67 | print("Image alignment complete...") 68 | 69 | # Save to file. 70 | if out_file[-4:] == ".mrc": 71 | if verbose: 72 | print("Saving to file as MRC...") 73 | acq_series.save_as_mrc(out_file=out_file) 74 | 75 | elif out_file[-4:] == ".tif" or out_file[-5:] == ".tiff": 76 | if verbose: 77 | print("Saving to file as TIFF...") 78 | acq_series.save_as_tif(out_file=out_file) 79 | 80 | else: 81 | raise Exception("Error: Out file type not recognized: " + str(out_file)) 82 | 83 | if verbose: 84 | print("Displaying goodbye message...") 85 | display_goodbye_message(out_path=out_file) 86 | 87 | 88 | def script_entry(): 89 | """ 90 | Entry point for Align Images script. Once pyTEM is installed, view script usage by running the following 91 | command in a terminal window: 92 | 93 | align_images --help 94 | 95 | """ 96 | parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter) 97 | parser.add_argument("-v", "--verbose", help="Increase verbosity; especially useful when debugging.", 98 | action="store_true") 99 | args = parser.parse_args() 100 | align_images(verbose=args.verbose) 101 | 102 | 103 | if __name__ == "__main__": 104 | align_images(verbose=True) 105 | -------------------------------------------------------------------------------- /pyTEM_scripts/bulk_carbon_analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import argparse 7 | import warnings 8 | 9 | from pyTEM.lib.Acquisition import Acquisition 10 | from pyTEM.lib.AcquisitionSeries import AcquisitionSeries 11 | from pyTEM_scripts.lib.bulk_carbon_analysis.GetCorrectionalFactors import GetCorrectionalFactors 12 | 13 | from pyTEM_scripts.lib.bulk_carbon_analysis.GetInOutFiles import GetInOutFiles 14 | from pyTEM_scripts.lib.bulk_carbon_analysis.display_goodbye_message import display_goodbye_message 15 | 16 | DESCRIPTION = "Given 16 natural light micrographs of a bulk carbon sample sandwiched between polarizers of varying " \ 17 | "cross, produce both anisotropy and orientation maps. " \ 18 | "\n\n" \ 19 | "Since input files will be read in alphabetical order (regardless of the order in which they are " \ 20 | "selected), the files must be titled such that they are ordered as follows when sorted alphabetically: " \ 21 | "\n" \ 22 | "\n Polarizer Angle [deg] Analyzer Angle [deg] Suggested Filename Prefix " \ 23 | "\n 0 0 11_ OR 0_0_" \ 24 | "\n 0 45 12_ OR 0_45_" \ 25 | "\n 0 90 13_ OR 0_90_" \ 26 | "\n 0 135 14_ OR 0_135_" \ 27 | "\n\n" \ 28 | "\n 45 0 21_ OR 45_0_" \ 29 | "\n 45 45 22_ OR 45_45_" \ 30 | "\n 45 90 23_ OR 45_90_" \ 31 | "\n 45 135 24_ OR 45_135_" \ 32 | "\n\n" \ 33 | "\n 90 0 31_ OR 90_0_" \ 34 | "\n 90 45 32_ OR 90_45_" \ 35 | "\n 90 90 33_ OR 90_90_" \ 36 | "\n 90 135 34_ OR 90_135_" \ 37 | "\n\n" \ 38 | "\n 135 0 41_ OR 135_0_" \ 39 | "\n 135 45 42_ OR 135_45_" \ 40 | "\n 135 90 43_ OR 135_90_" \ 41 | "\n 135 135 44_ OR 135_135_" \ 42 | "\n\n" \ 43 | "Notice that putting the suggested file name prefixes at the beginning of your input file names " \ 44 | "ensures the input files are read in the expected order." \ 45 | "\n\n" \ 46 | "We use the technique explained in the following paper:" \ 47 | "\n Gillard, Adrien & Couégnat, Guillaume & Caty, O. & Allemand, Alexandre & P, Weisbecker & " \ 48 | "\n Vignoles, Gerard. (2015). A quantitative, space-resolved method for optical anisotropy " \ 49 | "\n estimation in bulk carbons. Carbon. 91. 423-435. 10.1016/j.carbon.2015.05.005." \ 50 | "\n" \ 51 | "\nIn line with the Gillard et al. notation, image variables are labeled and referenced as follows:" \ 52 | "\n" \ 53 | "\n Analyzer angle" \ 54 | "\n 0 deg 45 deg 90 deg 135 deg" \ 55 | "\n Polarizer angle 0 deg 11 12 13 14" \ 56 | "\n 45 deg 21 22 23 24" \ 57 | "\n 90 deg 31 32 33 34" \ 58 | "\n 135 deg 41 42 43 44" 59 | 60 | 61 | def bulk_carbon_analysis(verbose: bool = False): 62 | """ 63 | Given 16 natural light micrographs of a bulk carbon sample sandwiched between polarizers of varying cross, produce 64 | both anisotropy and orientation maps. For more information, please refer to the above description, or view using: 65 | 66 | bulk_carbon_analysis --help 67 | 68 | :param verbose: bool: 69 | Print out extra information. Useful for debugging. 70 | 71 | :return: None. The resulting anisotropy and orientation maps are saved to file at the locations requested by the 72 | user. 73 | """ 74 | # Get the in and out file info from the user. 75 | if verbose: 76 | print("Prompting the user for in and out file path info...") 77 | in_files, anisotropy_out_path, orientation_out_path = GetInOutFiles().run() 78 | 79 | if len(in_files) != 16: 80 | raise Exception("Error: bulk_carbon_analysis() received the wrong number of input files. " 81 | "Expected 16, received " + str(len(in_files)) + ".") 82 | 83 | # Prompt the user for correctional factors 84 | if verbose: 85 | print("Prompting the user for correctional factors...") 86 | correctional_factors = GetCorrectionalFactors().run() 87 | 88 | # Use the pyTEM Acquisition and AcquisitionSeries classes to make our lives a little easier. 89 | if verbose: 90 | print("\nLoading the 16 in files into a AcquisitionSeries object...") 91 | acq_series = AcquisitionSeries() 92 | for i, file in enumerate(in_files): 93 | if verbose: 94 | print(" Loading image " + str(i + 1) + " out of 16...") 95 | acq_series.append(Acquisition(file)) 96 | 97 | # There shouldn't be any drift, but sometimes there is some. 98 | if verbose: 99 | print("\nAligning series...") 100 | acq_series.align() 101 | 102 | warnings.warn("This script is still under development and is not yet fully implemented.") # TODO 103 | 104 | display_goodbye_message(anisotropy_out_path=anisotropy_out_path, orientation_out_path=orientation_out_path) 105 | 106 | 107 | def script_entry(): 108 | """ 109 | Entry point for the Bulk Carbon Analysis script. Once pyTEM is installed, view script usage by running the following 110 | command in a terminal window: 111 | 112 | bulk_carbon_analysis --help 113 | 114 | """ 115 | parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter) 116 | parser.add_argument("-v", "--verbose", help="Increase verbosity, especially useful when debugging.", 117 | action="store_true") 118 | args = parser.parse_args() 119 | 120 | bulk_carbon_analysis(verbose=args.verbose) 121 | 122 | 123 | if __name__ == "__main__": 124 | bulk_carbon_analysis() 125 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/lib/__init__.py -------------------------------------------------------------------------------- /pyTEM_scripts/lib/align_images/GetInOutFile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | import tkinter as tk 10 | 11 | from tkinter import ttk, filedialog 12 | from typing import Tuple, Union 13 | from pathlib import Path 14 | 15 | from pyTEM_scripts.lib.micro_ed.add_basf_icon_to_tkinter_window import add_basf_icon_to_tkinter_window 16 | 17 | 18 | class GetInOutFile: 19 | """ 20 | Display a Tkinter window, and get the in and out file info from the user. 21 | 22 | Does nothing to verify file type. 23 | """ 24 | 25 | def __init__(self): 26 | self.in_file = "" 27 | self.out_directory = str(Path.home()) 28 | 29 | def run(self) -> Tuple[Union[str, Tuple[str]], str]: 30 | """ 31 | Get the in and out file paths. 32 | 33 | :return: 34 | in_file: str or tuple of strings: Path(s) to the file(s) containing the image(s) or image stack that the 35 | user wants to read in and align. 36 | out_file: str: Path to the destination where the user wants to store the file containing the aligned 37 | image stack. 38 | """ 39 | root = tk.Tk() 40 | style = ttk.Style() 41 | 42 | window_width = 500 43 | label_font = None # Just use the default font 44 | label_font_size = 12 45 | 46 | root.title("Align Images") 47 | add_basf_icon_to_tkinter_window(root) 48 | 49 | def get_single_in_file(): 50 | """ 51 | Have the user select a single file. 52 | :return: None, but the in_file attribute is updated with the path to the selected file (as a string). 53 | """ 54 | # Open file explorer, prompting the user to select a file. 55 | leaf = tk.Tk() 56 | leaf.title("Please select a single file containing an image stack you would like to align.") 57 | add_basf_icon_to_tkinter_window(leaf) 58 | leaf.geometry("{width}x{height}".format(width=500, height=leaf.winfo_height())) 59 | self.in_file = filedialog.askopenfilename() 60 | 61 | # Make sure we actually got an in file and the user didn't cancel 62 | if self.in_file not in {"", " "}: 63 | # Update the in_file label with the currently selected file. 64 | in_file_label.config(text="1 file selected:") 65 | list_of_in_files_label.config(text=self.in_file) 66 | in_file_label.configure(foreground="black") 67 | 68 | # Suggest an outfile directory based on the input directory. 69 | self.out_directory = os.path.dirname(self.in_file) 70 | out_directory_label.config(text=self.out_directory + "/") 71 | 72 | # Suggest an out file to have the same name as the in file. 73 | suggested_out_file_name, _ = os.path.splitext(os.path.basename(self.in_file)) 74 | out_file_name_entry_box.delete(0, tk.END) 75 | out_file_name_entry_box.insert(0, str(suggested_out_file_name) + "_aligned") 76 | 77 | # As long as we have an in-file, we are now okay to go ahead with the alignment. 78 | go_button.config(state=tk.NORMAL) 79 | 80 | leaf.destroy() 81 | 82 | def get_multiple_in_files(): 83 | """ 84 | Have the user select multiple files. 85 | :return: None, but the in_file attribute is updated with the path to the selected files (as a tuple of 86 | strings) 87 | """ 88 | # Open file explorer, prompting the user to select some files. 89 | leaf = tk.Tk() 90 | leaf.title("Please select the images you would like to align.") 91 | add_basf_icon_to_tkinter_window(leaf) 92 | leaf.geometry("{width}x{height}".format(width=500, height=leaf.winfo_height())) 93 | self.in_file = filedialog.askopenfilenames() 94 | 95 | # Make sure we actually got an in file and the user didn't cancel 96 | if self.in_file not in {"", " "}: 97 | # Update the in_file label with the currently selected list of files. 98 | if len(self.in_file) == 1: 99 | in_file_label.config(text="1 file selected:") 100 | list_of_in_files_label.config(text=self.in_file[0]) 101 | elif len(self.in_file) == 2: 102 | in_file_label.config(text="2 files selected:") 103 | list_of_in_files_label.config(text=self.in_file[0] + "\n" + self.in_file[1]) 104 | else: 105 | # Just print out the first and last file. 106 | in_file_label.config(text=str(len(self.in_file)) + " files selected:") 107 | list_of_in_files_label.config(text=self.in_file[0] + "\n" + "*" + "\n" + "*" + "\n" + "*" 108 | + "\n" + self.in_file[-1]) 109 | in_file_label.configure(foreground="black") 110 | 111 | # Suggest an outfile directory based on first input file. 112 | self.out_directory = os.path.dirname(self.in_file[0]) 113 | out_directory_label.config(text=self.out_directory + "/") 114 | 115 | # Suggest an out file name based on the first file in the series. 116 | suggested_out_file_name, _ = os.path.splitext(os.path.basename(self.in_file[0])) 117 | out_file_name_entry_box.delete(0, tk.END) 118 | out_file_name_entry_box.insert(0, str(suggested_out_file_name) + "_aligned") 119 | 120 | # As long as we have an in-file, we are now okay to go ahead with the alignment. 121 | go_button.config(state=tk.NORMAL) 122 | 123 | leaf.destroy() 124 | 125 | def change_out_directory(): 126 | """ 127 | Change/update the out directory. 128 | :return: None, but the out_directory attribute is updated with the new out directory of the users choosing. 129 | """ 130 | leaf = tk.Tk() 131 | leaf.title("Please select an out directory.") 132 | add_basf_icon_to_tkinter_window(leaf) 133 | leaf.geometry("{width}x{height}".format(width=500, height=leaf.winfo_height())) 134 | 135 | self.out_directory = filedialog.askdirectory() 136 | out_directory_label.configure(text=self.out_directory + "/") 137 | leaf.destroy() 138 | 139 | in_file_message = ttk.Label(root, text="Which images would you like to align?", wraplength=window_width, 140 | justify='center', font=(label_font, label_font_size, 'bold')) 141 | in_file_message.grid(column=0, columnspan=3, row=0, padx=5, pady=5) 142 | 143 | # Create a button that, when clicked, will prompt the user to select a single file containing the image 144 | # stack that would like to align. 145 | single_in_file_button = ttk.Button(root, text="Select a Single Image Stack", 146 | command=lambda: get_single_in_file(), style="big.TButton") 147 | single_in_file_button.grid(column=0, columnspan=3, row=1, padx=5, pady=5) 148 | 149 | # Create a button that, when clicked, will prompt the user to select the images they would like to align. 150 | multiple_in_file_button = ttk.Button(root, text="Select Multiple Single Image Files", 151 | command=lambda: get_multiple_in_files(), style="big.TButton") 152 | multiple_in_file_button.grid(column=0, columnspan=3, row=2, padx=5, pady=5) 153 | 154 | # Display the in file 155 | in_file_label = ttk.Label(root, text="No files selected.", justify='center', wraplength=window_width, 156 | font=(label_font, label_font_size, 'bold')) 157 | in_file_label.configure(foreground="red") 158 | in_file_label.grid(column=0, columnspan=3, row=3, padx=5, pady=5) 159 | 160 | list_of_in_files_label = ttk.Label(root, text="", justify='center', wraplength=window_width, 161 | font=(label_font, label_font_size)) 162 | list_of_in_files_label.grid(column=0, columnspan=3, row=4, padx=5, pady=5) 163 | 164 | out_file_message = ttk.Label(root, text="Where would you like to save the results?", justify='center', 165 | font=(label_font, label_font_size, 'bold'), wraplength=window_width) 166 | out_file_message.grid(column=0, columnspan=3, row=5, padx=5, pady=5) 167 | 168 | # Create a button that, when clicked, will update the out_file directory 169 | out_file_button = ttk.Button(root, text="Update Out Directory", command=lambda: change_out_directory(), 170 | style="big.TButton") 171 | out_file_button.grid(column=0, columnspan=3, row=6, padx=5, pady=5) 172 | 173 | # Label the filename box with the out directory 174 | out_directory_label = ttk.Label(root, text=self.out_directory + "\\") 175 | out_directory_label.grid(column=0, row=7, sticky="e", padx=5, pady=5) 176 | 177 | # Create an entry box for the user to enter the out file name. 178 | out_file_name = tk.StringVar() 179 | out_file_name_entry_box = ttk.Entry(root, textvariable=out_file_name) 180 | out_file_name_entry_box.grid(column=1, row=7, padx=5, pady=5) 181 | 182 | # Add a dropdown menu to get the file extension 183 | file_extension_options = ['.tif', '.mrc'] 184 | file_extension = tk.StringVar() 185 | file_extension_menu = ttk.OptionMenu(root, file_extension, file_extension_options[0], *file_extension_options) 186 | file_extension_menu.grid(column=2, row=7, sticky="w", padx=5, pady=5) 187 | 188 | # Create go and exit buttons 189 | go_button = ttk.Button(root, text="Go", command=lambda: root.destroy(), style="big.TButton") 190 | go_button.grid(column=0, columnspan=3, row=8, padx=5, pady=5) 191 | go_button.config(state=tk.DISABLED) 192 | exit_button = ttk.Button(root, text="Quit", command=lambda: sys.exit(1), style="big.TButton") 193 | exit_button.grid(column=0, columnspan=3, row=9, padx=5, pady=5) 194 | 195 | style.configure('big.TButton', font=(None, 10), foreground="blue4") 196 | root.eval('tk::PlaceWindow . center') # Center the window on the screen 197 | 198 | root.protocol("WM_DELETE_WINDOW", lambda: sys.exit(1)) 199 | root.mainloop() 200 | 201 | # Build and return the complete path 202 | out_path = self.out_directory + "/" + str(out_file_name.get()) + str(file_extension.get()) 203 | return self.in_file, out_path 204 | 205 | 206 | if __name__ == "__main__": 207 | in_file, out_file = GetInOutFile().run() 208 | 209 | if isinstance(in_file, str): 210 | print("Received a single in file: " + in_file) 211 | 212 | elif isinstance(in_file, Tuple): 213 | print("Received multiple in files:") 214 | for file in in_file: 215 | print(file) 216 | 217 | else: 218 | raise Exception("Error: in_file type not recognized.") 219 | 220 | print("\nOut file: " + out_file) 221 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/align_images/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/lib/align_images/__init__.py -------------------------------------------------------------------------------- /pyTEM_scripts/lib/align_images/display_goodbye_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import pathlib 7 | import sys 8 | import tkinter as tk 9 | 10 | from tkinter import ttk 11 | from typing import Union 12 | 13 | from pyTEM_scripts.lib.micro_ed.add_basf_icon_to_tkinter_window import add_basf_icon_to_tkinter_window 14 | 15 | 16 | def display_goodbye_message(out_path: Union[pathlib.Path, str]): 17 | """ 18 | Display a goodbye message, 19 | - thanking the user, 20 | - letting them know where to find the alignment results, 21 | - and prompting them to report any issues on the GitHub. 22 | 23 | :param out_path: str or path: 24 | Path to the alignment results. 25 | 26 | :return: None. 27 | """ 28 | root = tk.Tk() 29 | style = ttk.Style() 30 | window_width = 650 31 | 32 | root.title("Mission success!") 33 | add_basf_icon_to_tkinter_window(root) 34 | 35 | upper_message = "Image Alignment Successful!" \ 36 | "\n\nThe results can be found here:" 37 | lower_message = "Thank you for using pyTEM, please report any issues on GitHub:" 38 | 39 | # Display the upper part of the message. 40 | upper_message_label = ttk.Label(root, text=upper_message, wraplength=window_width, font=(None, 15), 41 | justify='center') 42 | upper_message_label.grid(column=0, row=0, padx=5, pady=(5, 0)) 43 | 44 | # Display the out file path. 45 | out_path_label = ttk.Label(root, text=out_path, wraplength=window_width, font=(None, 15, 'bold'), 46 | justify='center') 47 | out_path_label.grid(column=0, row=1, padx=5, pady=(0, 5)) 48 | 49 | # Display the lower part of the message. 50 | upper_message_label = ttk.Label(root, text=lower_message, wraplength=window_width, font=(None, 15), 51 | justify='center') 52 | upper_message_label.grid(column=0, row=2, padx=5, pady=(5, 0)) 53 | 54 | # Display the link to the GitHub issues board. 55 | github_issues_label = ttk.Label(root, text="https://github.com/basf/pyTEM/issues", wraplength=window_width, 56 | font=(None, 15, 'bold'), justify='center') 57 | github_issues_label.grid(column=0, row=3, padx=5, pady=(0, 5)) 58 | 59 | # Create exit button. 60 | exit_button = ttk.Button(root, text="Exit", command=lambda: sys.exit(0), style="big.TButton") 61 | exit_button.grid(column=0, row=4, padx=5, pady=5) 62 | style.configure('big.TButton', font=(None, 10), foreground="blue4") 63 | 64 | # Center the window on the screen 65 | root.eval('tk::PlaceWindow . center') 66 | 67 | root.protocol("WM_DELETE_WINDOW", lambda: sys.exit(0)) 68 | root.mainloop() 69 | 70 | 71 | if __name__ == "__main__": 72 | display_goodbye_message(out_path="Some fake path...") 73 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/bulk_carbon_analysis/GetInOutFiles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | import tkinter as tk 10 | 11 | from tkinter import ttk, filedialog 12 | from typing import Tuple 13 | from pathlib import Path 14 | 15 | from pyTEM_scripts.lib.micro_ed.add_basf_icon_to_tkinter_window import add_basf_icon_to_tkinter_window 16 | 17 | 18 | class GetInOutFiles: 19 | """ 20 | Display a Tkinter window to help the user: 21 | 1. Select the 16 required natural light micrographs to use as input. 22 | 2. Select output file names for the resultant anisotropy and orientation maps. 23 | 24 | Does nothing to verify in file type nor image order. For more info on how images should be ordered, read the docs: 25 | 26 | bulk_carbon_analysis --help 27 | 28 | """ 29 | 30 | def __init__(self): 31 | self.in_files = () 32 | self.out_directory = str(Path.home()) 33 | 34 | def run(self) -> Tuple[Tuple, str, str]: 35 | """ 36 | Get the in and out file paths. 37 | 38 | :return: 39 | in_files: Tuple of strings: Paths to the 16 light microscopy input files. 40 | anisotropy_out_file: str: Path to the destination where the user wants to store the resultant anisotropy 41 | map. 42 | orientation_out_file: str: Path to the destination where the user wants to store the resultant orientation 43 | map. 44 | """ 45 | root = tk.Tk() 46 | style = ttk.Style() 47 | 48 | window_width = 500 49 | label_font = None # Just use the default font. 50 | label_font_size = 12 51 | 52 | root.title("Bulk Carbon Analysis") 53 | add_basf_icon_to_tkinter_window(root) 54 | 55 | def get_in_files(): 56 | """ 57 | Have the user select the 16 natural light microscopy in files. 58 | :return: None, but the in_files attribute is updated with the paths to the selected files (as a tuple of 59 | strings). 60 | """ 61 | # Open file explorer, prompting the user to select some files. 62 | leaf = tk.Tk() 63 | leaf.title("Please select all 16 light microscopy images.") 64 | add_basf_icon_to_tkinter_window(leaf) 65 | leaf.geometry("{width}x{height}".format(width=500, height=leaf.winfo_height())) 66 | in_files = filedialog.askopenfilenames() 67 | 68 | # Make sure we actually got an in file and the user didn't just cancel. 69 | if in_files not in {"", " "}: 70 | if len(in_files) == 16: 71 | # Then we are good, update the in_file label with the currently selected list of files. 72 | self.in_files = in_files 73 | 74 | # Just print out the first and last file. 75 | in_files_label.config(text=str(len(self.in_files)) + " files selected:") 76 | in_files_label.configure(foreground="black") 77 | list_of_in_files_label.config(text=self.in_files[0] + "\n" + "*" + "\n" + "*" + "\n" + "*" 78 | + "\n" + self.in_files[-1]) 79 | 80 | # Suggest an outfile directory based on first input file. 81 | self.out_directory = os.path.dirname(self.in_files[0]) 82 | anisotropy_out_directory_label.config(text=self.out_directory + "/") 83 | orientation_out_directory_label.config(text=self.out_directory + "/") 84 | 85 | # As long as we have an in-file, we are now okay to go ahead with the alignment. 86 | go_button.config(state=tk.NORMAL) 87 | 88 | else: 89 | # Remind the user that we are expecting exactly 16 files. 90 | in_files_label.config(text="Error: " + str(len(self.in_files)) + " files selected, but we " 91 | "require exactly 16.") 92 | in_files_label.config(foreground="red") 93 | list_of_in_files_label.config(text="") 94 | go_button.config(state=tk.DISABLED) 95 | 96 | else: 97 | pass # Don't update self.in_files 98 | 99 | leaf.destroy() 100 | 101 | def change_out_directory(): 102 | """ 103 | Change/update the out directory. 104 | :return: None, but the out_directory attribute is updated with the new out directory of the users choosing. 105 | """ 106 | leaf = tk.Tk() 107 | leaf.title("Please select an out directory.") 108 | add_basf_icon_to_tkinter_window(leaf) 109 | leaf.geometry("{width}x{height}".format(width=500, height=leaf.winfo_height())) 110 | 111 | self.out_directory = filedialog.askdirectory() 112 | anisotropy_out_directory_label.configure(text=self.out_directory + "/") 113 | orientation_out_directory_label.configure(text=self.out_directory + "/") 114 | leaf.destroy() 115 | 116 | in_file_message = ttk.Label(root, text="Which images would you like to align?", wraplength=window_width, 117 | justify='center', font=(label_font, label_font_size, 'bold')) 118 | in_file_message.grid(column=0, columnspan=3, row=0, padx=5, pady=5) 119 | 120 | # Create a button that, when clicked, will prompt the user to select the images they would like to align. 121 | select_in_files_button = ttk.Button(root, text="Select Light Microscopy Images", command=get_in_files, 122 | style="big.TButton") 123 | select_in_files_button.grid(column=0, columnspan=3, row=1, padx=5, pady=5) 124 | 125 | # We will display the selected in files here. At the beginning we will have no in files. 126 | in_files_label = ttk.Label(root, text="No files selected.", justify='center', wraplength=window_width, 127 | font=(label_font, label_font_size, 'bold')) 128 | in_files_label.configure(foreground="red") 129 | in_files_label.grid(column=0, columnspan=3, row=2, padx=5, pady=5) 130 | 131 | list_of_in_files_label = ttk.Label(root, text="", justify='center', wraplength=window_width, 132 | font=(label_font, label_font_size)) 133 | list_of_in_files_label.grid(column=0, columnspan=3, row=3, padx=5, pady=5) 134 | 135 | # Create a label asking for the anisotropy out file. 136 | out_file_label = ttk.Label(root, text="Where would you like to save results?", 137 | justify='center', font=(label_font, label_font_size, 'bold'), 138 | wraplength=window_width) 139 | out_file_label.grid(column=0, columnspan=3, row=4, padx=5, pady=5) 140 | 141 | # Create a button that, when clicked, will update the out_file directory. 142 | out_file_button = ttk.Button(root, text="Update Out Directory", command=lambda: change_out_directory(), 143 | style="big.TButton") 144 | out_file_button.grid(column=0, columnspan=3, row=6, padx=5, pady=5) 145 | 146 | file_extension_options = ['.tif', '.mrc', '.png', '.jpeg'] 147 | 148 | # Get the anisotropy out file path. 149 | anisotropy_out_file_label = ttk.Label(root, text="Anisotropy Map:", justify='center', 150 | font=(label_font, label_font_size - 2), wraplength=window_width) 151 | anisotropy_out_file_label.grid(column=0, columnspan=3, row=7, padx=5, pady=(5, 0)) 152 | anisotropy_out_file = tk.StringVar() 153 | anisotropy_out_directory_label = ttk.Label(root, text=self.out_directory + "\\") 154 | anisotropy_out_directory_label.grid(column=0, row=8, sticky="e", padx=5, pady=(0, 5)) 155 | anisotropy_out_file_entry_box = ttk.Entry(root, textvariable=anisotropy_out_file) 156 | anisotropy_out_file_entry_box.grid(column=1, row=8, padx=5, pady=(0, 5)) 157 | anisotropy_out_file_entry_box.insert(0, "anisotropy_map") 158 | anisotropy_file_extension = tk.StringVar() 159 | anisotropy_file_extension_menu = ttk.OptionMenu(root, anisotropy_file_extension, file_extension_options[0], 160 | *file_extension_options) 161 | anisotropy_file_extension_menu.grid(column=2, row=8, sticky="w", padx=5, pady=(0, 5)) 162 | 163 | # Get the orientation out file path. 164 | orientation_out_file_label = ttk.Label(root, text="Orientation Map:", justify='center', 165 | font=(label_font, label_font_size - 2), wraplength=window_width) 166 | orientation_out_file_label.grid(column=0, columnspan=3, row=9, padx=5, pady=(5, 0)) 167 | orientation_out_file = tk.StringVar() 168 | orientation_out_directory_label = ttk.Label(root, text=self.out_directory + "\\") 169 | orientation_out_directory_label.grid(column=0, row=10, sticky="e", padx=5, pady=(0, 5)) 170 | orientation_out_file_entry_box = ttk.Entry(root, textvariable=orientation_out_file) 171 | orientation_out_file_entry_box.grid(column=1, row=10, padx=5, pady=(0, 5)) 172 | orientation_out_file_entry_box.insert(0, "orientation_map") 173 | orientation_file_extension = tk.StringVar() 174 | orientation_file_extension_menu = ttk.OptionMenu(root, orientation_file_extension, file_extension_options[0], 175 | *file_extension_options) 176 | orientation_file_extension_menu.grid(column=2, row=10, sticky="w", padx=5, pady=(0, 5)) 177 | 178 | # Create go and exit buttons. 179 | go_button = ttk.Button(root, text="Go", command=lambda: root.destroy(), style="big.TButton") 180 | go_button.grid(column=0, columnspan=3, row=11, padx=5, pady=5) 181 | go_button.config(state=tk.DISABLED) 182 | exit_button = ttk.Button(root, text="Quit", command=lambda: sys.exit(1), style="big.TButton") 183 | exit_button.grid(column=0, columnspan=3, row=12, padx=5, pady=5) 184 | 185 | style.configure('big.TButton', font=(None, 10), foreground="blue4") 186 | root.eval('tk::PlaceWindow . center') # Center the window on the screen 187 | 188 | root.protocol("WM_DELETE_WINDOW", lambda: sys.exit(1)) 189 | root.mainloop() 190 | 191 | # Build and return the complete path 192 | full_anisotropy_out_path = self.out_directory + "/" + str(anisotropy_out_file.get()) \ 193 | + str(anisotropy_file_extension.get()) 194 | full_orientation_out_path = self.out_directory + "/" + str(anisotropy_out_file.get()) \ 195 | + str(orientation_file_extension.get()) 196 | return self.in_files, full_anisotropy_out_path, full_orientation_out_path 197 | 198 | 199 | if __name__ == "__main__": 200 | in_files_, anisotropy_out_path, orientation_out_path = GetInOutFiles().run() 201 | 202 | if isinstance(in_files_, Tuple): 203 | print("Received " + str(len(in_files_)) + " in files:") 204 | for file in in_files_: 205 | print(file) 206 | 207 | else: 208 | raise Exception("Error: in_files is not a Tuple.") 209 | 210 | print("\nAnisotropy out file path: " + anisotropy_out_path) 211 | print("\nOrientation out file path: " + orientation_out_path) 212 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/bulk_carbon_analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/lib/bulk_carbon_analysis/__init__.py -------------------------------------------------------------------------------- /pyTEM_scripts/lib/bulk_carbon_analysis/display_goodbye_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import pathlib 7 | import sys 8 | import tkinter as tk 9 | 10 | from tkinter import ttk 11 | from typing import Union 12 | 13 | from pyTEM_scripts.lib.micro_ed.add_basf_icon_to_tkinter_window import add_basf_icon_to_tkinter_window 14 | 15 | 16 | def display_goodbye_message(anisotropy_out_path: Union[pathlib.Path, str], 17 | orientation_out_path: Union[pathlib.Path, str]): 18 | """ 19 | Display a goodbye message, 20 | - thanking the user, 21 | - letting them know where to find the anisotropy and orientation map results, 22 | - and prompting them to report any issues on the GitHub. 23 | 24 | :param anisotropy_out_path: str or path: 25 | Path to the resultant anisotropy map. 26 | :param orientation_out_path: str or path: 27 | Path to the resultant orientation map. 28 | 29 | :return: None. 30 | """ 31 | root = tk.Tk() 32 | style = ttk.Style() 33 | max_window_width = 650 34 | 35 | root.title("Mission success!") 36 | add_basf_icon_to_tkinter_window(root) 37 | 38 | success_message_label = ttk.Label(root, text="Bulk carbon analysis successful!", wraplength=max_window_width, 39 | font=(None, 15), justify='center') 40 | success_message_label.grid(column=0, row=0, padx=5, pady=5) 41 | 42 | # Display the anisotropy out file path. 43 | anisotropy_out_label = ttk.Label(root, text="The anisotropy map can be found here:", wraplength=max_window_width, 44 | font=(None, 15), justify='center') 45 | anisotropy_out_label.grid(column=0, row=1, padx=5, pady=(5, 0)) 46 | anisotropy_out_path_label = ttk.Label(root, text=str(anisotropy_out_path), wraplength=max_window_width, 47 | font=(None, 15, 'bold'), justify='center') 48 | anisotropy_out_path_label.grid(column=0, row=2, padx=5, pady=(0, 5)) 49 | 50 | # Display the orientation out file path. 51 | orientation_out_label = ttk.Label(root, text="The orientation map can be found here:", wraplength=max_window_width, 52 | font=(None, 15), justify='center') 53 | orientation_out_label.grid(column=0, row=3, padx=5, pady=(5, 0)) 54 | orientation_out_path_label = ttk.Label(root, text=str(orientation_out_path), wraplength=max_window_width, 55 | font=(None, 15, 'bold'), justify='center') 56 | orientation_out_path_label.grid(column=0, row=4, padx=5, pady=(0, 5)) 57 | 58 | # Display messages thanking the user and prompting them to report any issues on GitHub. 59 | thanks_message_label = ttk.Label(root, text="Thank you for using pyTEM, please report any issues on GitHub:", 60 | wraplength=max_window_width, font=(None, 15), justify='center') 61 | thanks_message_label.grid(column=0, row=5, padx=5, pady=(5, 0)) 62 | github_issues_label = ttk.Label(root, text="https://github.com/basf/pyTEM/issues", wraplength=max_window_width, 63 | font=(None, 15, 'bold'), justify='center') 64 | github_issues_label.grid(column=0, row=6, padx=5, pady=(0, 5)) 65 | 66 | # Create exit button. 67 | exit_button = ttk.Button(root, text="Exit", command=lambda: sys.exit(0), style="big.TButton") 68 | exit_button.grid(column=0, row=7, padx=5, pady=5) 69 | style.configure('big.TButton', font=(None, 10), foreground="blue4") 70 | 71 | # Center the window on the screen 72 | root.eval('tk::PlaceWindow . center') 73 | 74 | root.protocol("WM_DELETE_WINDOW", lambda: sys.exit(0)) 75 | root.mainloop() 76 | 77 | 78 | if __name__ == "__main__": 79 | display_goodbye_message(anisotropy_out_path="Some fake anisotropy path...", 80 | orientation_out_path="Some fake orientation path...") 81 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/lib/micro_ed/__init__.py -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/add_basf_icon_to_tkinter_window.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import os 7 | import tkinter as tk 8 | BASEDIR = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def add_basf_icon_to_tkinter_window(root: tk.Tk) -> None: 12 | """ 13 | Add the BASF ico to Tkinter window. 14 | :param root: tkinter window: 15 | The tkinter window of which you want to add the BASF icon. 16 | :return: None. 17 | """ 18 | path_to_ico = os.path.join(BASEDIR, "ico/BASF.ico") 19 | root.iconbitmap(path_to_ico) 20 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/build_full_shifts_array.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import math 7 | 8 | import numpy as np 9 | 10 | from typing import Union 11 | from numpy.typing import ArrayLike 12 | from scipy.interpolate import interp1d 13 | 14 | from pyTEM_scripts.lib.micro_ed.find_bound_indicies import find_bound_indices 15 | 16 | 17 | def build_full_shift_array(alphas: ArrayLike, samples: ArrayLike, shifts_at_samples: ArrayLike, verbose: bool = False, 18 | interpolation_scope: str = "local", kind: Union[str, int] = "linear") -> np.ndarray: 19 | """ 20 | We need compensatory image shifts at all values of alphas, but right now we only have shifts at for a sample of 21 | values. Using linear interpolation, build up the full shifts array. 22 | 23 | Interpolation is performed using scipy.interpolate.interp1d(). Refer to these docs for more info regarding the 24 | different kinds of interpolation that are supported. 25 | 26 | :param alphas: array of floats: 27 | Array of angles (in degrees) at which we need image shifts. 28 | :param samples: array of floats: 29 | Array of angles (in degrees) at which we have taken sample images and have image shifts. 30 | (usually computed directly from calibration images by obtain_shifts()). 31 | :param shifts_at_samples: array of float tuples: 32 | Array of tuples of the form (x, y) where x and y are the x-axis and y-axis image shifts (respectively) at the 33 | corresponding angle in samples. 34 | 35 | :param kind: str or int (optional; default is 'linear'): 36 | Specifies the kind of interpolation as a string or as an integer specifying the order of the spline 37 | interpolator to use. 38 | :param interpolation_scope: str (optional; default is 'local'): 39 | Interpolation scope, one of: 40 | - 'linear': Interpolate using only the adjacent (local) values from the sample array that bracket alpha. 41 | Because we only use two values, interpolation will always end up linear, regardless of kind. 42 | - 'global': Interpolate using all the values in the sample array. 43 | :param verbose: bool (optional; default is False): 44 | Print out extra information. Useful for debugging. 45 | 46 | :return: array: 47 | Array of tuples of the form (x, y) where x and y are the x-axis and y-axis image shifts (respectively) at the 48 | corresponding angle in alphas. The returned array of shifts is the same length as the provided alpha array. 49 | """ 50 | interpolation_scope = str(interpolation_scope).lower() 51 | shifts = np.full(shape=len(alphas), dtype=(float, 2), fill_value=np.nan) # Preallocate 52 | 53 | # Build global interpolators for both the x-axis and y-axis image shifts 54 | global_x_shift_interpolator = None # Warning suppression 55 | global_y_shift_interpolator = None 56 | if interpolation_scope == "global": 57 | global_x_shift_interpolator = interp1d(x=samples, y=shifts_at_samples[:, 0], kind=kind) 58 | global_y_shift_interpolator = interp1d(x=samples, y=shifts_at_samples[:, 1], kind=kind) 59 | 60 | for alpha_shift_idx, alpha in enumerate(alphas): 61 | if alpha in samples: 62 | # Then we already know what the shift needs to be, just find it and insert it into the array. 63 | sample_idx = np.where(samples == alpha) 64 | shifts[alpha_shift_idx] = shifts_at_samples[sample_idx] 65 | 66 | if verbose: 67 | print(str(alpha) + " is in the samples array, inserting directly inserting the corresponding shifts " 68 | "into the array at " + str(alpha_shift_idx)) 69 | 70 | else: 71 | if verbose: 72 | print(str(alpha) + " is not in the samples array, performing a " + interpolation_scope 73 | + " interpolation of kind: " + str(kind)) 74 | 75 | if interpolation_scope == "global": 76 | # Use the global interpolators built above 77 | shifts[alpha_shift_idx] = (global_x_shift_interpolator(alpha), global_y_shift_interpolator(alpha)) 78 | 79 | elif interpolation_scope == "local": 80 | 81 | # Build and use interpolators using only the local (adjacent) values in sample. 82 | lower_bound_idx, upper_bound_idx = find_bound_indices(array=samples, value=alpha) 83 | 84 | local_x_shift_interpolator = interp1d(x=samples[lower_bound_idx:upper_bound_idx + 1], 85 | y=shifts_at_samples[lower_bound_idx:upper_bound_idx + 1, 0], 86 | kind=kind) 87 | local_y_shift_interpolator = interp1d(x=samples[lower_bound_idx:upper_bound_idx + 1], 88 | y=shifts_at_samples[lower_bound_idx:upper_bound_idx + 1, 1], 89 | kind=kind) 90 | shifts[alpha_shift_idx] = (local_x_shift_interpolator(alpha), local_y_shift_interpolator(alpha)) 91 | 92 | else: 93 | raise Exception("Error: interpolation_scope not recognized.") 94 | 95 | return shifts 96 | 97 | 98 | if __name__ == "__main__": 99 | 100 | start_alpha = 23 101 | stop_alpha = -35 102 | step_alpha = 1 103 | 104 | num_alpha = abs(int((stop_alpha - start_alpha) / step_alpha)) + 1 105 | alpha_arr = np.linspace(start=start_alpha, stop=stop_alpha, num=num_alpha, endpoint=True) 106 | # alphas_ = alpha_arr[0:-1] + step_alpha / 2 107 | alphas_ = alpha_arr 108 | 109 | print("\n Alphas:") 110 | print(alphas_) 111 | 112 | # Build pretend samples information (the x data) 113 | correction_shift_interval = 5 # deg 114 | total_tilt_range = abs(stop_alpha - start_alpha) 115 | num_images_required = math.ceil(total_tilt_range / correction_shift_interval) + 1 116 | samples_, step = np.linspace(start=start_alpha, stop=stop_alpha, num=num_images_required, 117 | endpoint=True, retstep=True) 118 | 119 | print("\nSamples:") 120 | print(samples_) 121 | 122 | # Build pretend shifts at samples information (the y data) 123 | x_shifts = np.linspace(start=-50, stop=40, num=num_images_required, endpoint=True) 124 | y_shifts = np.linspace(start=35, stop=-37, num=num_images_required, endpoint=True) 125 | shifts_at_samples_ = np.full(shape=num_images_required, dtype=(float, 2), fill_value=0.0) # All zero. 126 | for i in range(len(samples_)): 127 | shifts_at_samples_[i] = (x_shifts[i], y_shifts[i]) 128 | 129 | print("\nShifts at samples:") 130 | print(shifts_at_samples_) 131 | 132 | full_shift_array = build_full_shift_array(alphas=alphas_, samples=samples_, shifts_at_samples=shifts_at_samples_, 133 | kind='linear', interpolation_scope='global', verbose=True) 134 | print("Using global interpolation:") 135 | print(full_shift_array) 136 | 137 | full_shift_array = build_full_shift_array(alphas=alphas_, samples=samples_, shifts_at_samples=shifts_at_samples_, 138 | kind='linear', interpolation_scope='local', verbose=True) 139 | print("Using local interpolation:") 140 | print(full_shift_array) 141 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/closest_number.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: https://www.geeksforgeeks.org/find-number-closest-n-divisible-m/ 3 | Date: Retrieved Summer 2021 4 | """ 5 | 6 | 7 | def closest_number(n: float, m: float) -> float: 8 | """ 9 | Find the number closest to n that is divisible by m. 10 | 11 | :return: float: 12 | The number closest to n that is divisible by m. 13 | """ 14 | q = int(n / m) # Find the quotient 15 | 16 | # 1st possible closest number 17 | n1 = m * q 18 | 19 | # 2nd possible closest number 20 | if (n * m) > 0: 21 | n2 = (m * (q + 1)) 22 | else: 23 | n2 = (m * (q - 1)) 24 | 25 | # if true, then n1 is the closest number 26 | if abs(n - n1) < abs(n - n2): 27 | return n1 28 | 29 | # else n2 is the closest number 30 | return n2 31 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/exit_script.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import sys 7 | import warnings 8 | 9 | from typing import Union 10 | 11 | from pyTEM.Interface import Interface 12 | 13 | 14 | def exit_script(microscope: Union[Interface, None], status: int) -> None: 15 | """ 16 | Return the microscope to a safe state, zero the alpha tilt, zero the image and beam shifts, and restore the 17 | projection mode to 'imaging'. 18 | 19 | :param microscope: pyTEM.Interface (or None): 20 | The microscope interface. 21 | 22 | :param status: int: 23 | Exit status, one of: 24 | 0: Success 25 | 1: Early exit (script not yet complete) / Failure 26 | """ 27 | if microscope is not None: 28 | warnings.warn("Returning the microscope to a safe state, zeroing the alpha tilt, " 29 | "zeroing the image and beam shifts, and restoring the projection mode to 'imaging'...") 30 | microscope.make_safe() 31 | microscope.set_projection_mode(new_projection_mode="imaging") 32 | microscope.set_stage_position_alpha(alpha=0) 33 | microscope.zero_shifts() 34 | 35 | sys.exit(status) 36 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/find_bound_indicies.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | from typing import Any, Tuple 7 | 8 | import numpy as np 9 | from numpy.typing import ArrayLike 10 | 11 | 12 | def find_bound_indices(array: ArrayLike, value: Any) -> Tuple[float, float]: 13 | """ 14 | Find the indices of the elements in array that bound the value. 15 | 16 | :param array: np.array: 17 | A sorted array of values. Can be sorted either least -> greatest (results when titling neg -> pos) or 18 | greatest -> least (results when tilting pos -> neg). 19 | :param value: 20 | A value in array. 21 | 22 | :return: 23 | lower_idx: float: The index of the element of array that bounds value on one side. 24 | upper_idx: float: The index of the element of array that bounds value on the other side. 25 | """ 26 | if array.ndim != 1: 27 | raise Exception("Error: find_bound_indices() only works for 1-dimensional arrays.") 28 | 29 | is_sorted = np.all(array[:-1] <= array[1:]) 30 | is_reverse_sorted = np.all(array[:-1] >= array[1:]) 31 | 32 | if is_sorted: 33 | # Find the index at which insertion would persevere the ordering, we need this index and the one before 34 | insertion_idx = np.searchsorted(array, value, side="right") 35 | return insertion_idx - 1, insertion_idx 36 | 37 | elif is_reverse_sorted: 38 | # Find the index at which insertion would persevere the ordering, we need this index and the one before 39 | # Notice we need to search the reverse array and subtract the result from the length of the array 40 | insertion_idx = len(array) - np.searchsorted(np.flip(array), value, side="right") 41 | return insertion_idx - 1, insertion_idx 42 | 43 | else: 44 | raise Exception("Error: array is not sorted.") 45 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/hyperspy_warnings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import hyperspy.api as hs 7 | 8 | 9 | def turn_off_hyperspy_warnings(): 10 | """ 11 | By default, HyperSpy warns the user if one of the GUI packages is not installed. 12 | This function turns these warnings off. 13 | :return: None. 14 | """ 15 | hs.preferences.GUIs.warn_if_guis_are_missing = False 16 | hs.preferences.save() 17 | 18 | 19 | def turn_on_hyperspy_warnings(): 20 | """ 21 | Turn on Hyperspy warnings for missing GUI packages. 22 | :return: None. 23 | """ 24 | hs.preferences.GUIs.warn_if_guis_are_missing = True 25 | hs.preferences.save() 26 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/ico/BASF.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/lib/micro_ed/ico/BASF.ico -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/ico/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/lib/micro_ed/ico/__init__.py -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/opposite_signs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | from typing import Union 7 | 8 | 9 | def opposite_signs(x: Union[int, float], y: Union[int, float]) -> bool: 10 | """ 11 | Check if two numbers have the same sign 12 | :param x: int or float: 13 | The number to compare against y. 14 | :param y: int of float: 15 | The number to compare against x. 16 | :return: bool: 17 | True: x and y have opposite signs. 18 | False: x and y have the same sign. 19 | """ 20 | return (y >= 0) if (x < 0) else (y < 0) 21 | 22 | 23 | if __name__ == "__main__": 24 | print(opposite_signs(x=9, y=0.1)) 25 | print(opposite_signs(x=-1, y=-78)) 26 | print(opposite_signs(x=4, y=-8)) 27 | print(opposite_signs(x=-4, y=8)) 28 | -------------------------------------------------------------------------------- /pyTEM_scripts/lib/micro_ed/powspace.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import numpy as np 7 | 8 | 9 | def powspace(start, stop, power, num): 10 | """ 11 | Like numpy.linspace - except returns numbers spaced on a power scale. 12 | 13 | Only works for positive start/stop values! 14 | 15 | :param start: float: 16 | The starting value of the sequence. 17 | :param stop: float: 18 | The end value of the sequence 19 | :param power: float: 20 | The power scale to use. Notice, 21 | - powers greater than 1 result in values spaced increasingly farther apart towards end the array near 'stop', 22 | - power = 1 results in evenly (linearly) spaced values, 23 | - and powers less than 1 result in values spaced increasingly closer together towards end the array near 24 | 'start'. 25 | :param num: int: 26 | Number of samples to generate. 27 | 28 | :return: np.array: 29 | An array of values from start to stop (inclusive) with num values spaced on a power scale. 30 | """ 31 | if start < 0 or start < 0: 32 | raise Exception("Error: powspace() requires positive start/stop values.") 33 | if power <= 0: 34 | raise Exception("Error: powspace() requires a positive (non-zero) power.") 35 | 36 | start = np.power(start, 1/float(power)) 37 | stop = np.power(stop, 1/float(power)) 38 | 39 | return np.power(np.linspace(start, stop, num=num), power) 40 | -------------------------------------------------------------------------------- /pyTEM_scripts/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/test/__init__.py -------------------------------------------------------------------------------- /pyTEM_scripts/test/argparse_test.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def my_func(verbose: bool = False): 5 | if verbose: 6 | print("My function called with verbose=True.") 7 | else: 8 | print("My function called with verbose=False.") 9 | 10 | 11 | def script_entry(): 12 | print("-- calling from script_entry --") 13 | parser = argparse.ArgumentParser(description='Description of your program') 14 | parser.add_argument("-v", "--verbose", help="increase output verbosity", 15 | action="store_true") 16 | args = parser.parse_args() 17 | print("args: " + str(args)) 18 | my_func(verbose=args.verbose) 19 | 20 | 21 | if __name__ == "__main__": 22 | print("-- calling from main --") 23 | my_func(verbose=True) 24 | -------------------------------------------------------------------------------- /pyTEM_scripts/test/micro_ed/AcquisitionSeriesProperties.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | from numpy.typing import ArrayLike 7 | 8 | from pyTEM.lib.tem_tilt_speed import tem_tilt_speed 9 | 10 | 11 | class AcquisitionSeriesProperties: 12 | """ 13 | Basically just a datastructure to hold tilt series acquisition properties. 14 | 15 | Public Attributes: 16 | camera_name: str: The name of the camera being used. 17 | alpha_arr: array of floats: An complete array of the tilt acquisition's alpha start-stop values. 18 | alpha_step: float: Degrees between adjacent acquisitions. 19 | alphas: array of floats: The middle alpha values of the tilt acquisition. 20 | integration_time: int: Total exposure time for a single image in seconds. 21 | sampling: str: Photo resolution. 22 | tilt_speed: float: Alpha fractional tilt speed required by Interface.set_stage_position(). 23 | 24 | Protected Attributes: 25 | None. 26 | 27 | Private Attributes: 28 | None. 29 | """ 30 | 31 | def __init__(self, camera_name: str, alpha_arr: ArrayLike, integration_time: float = 3, sampling: str = '1k'): 32 | """ 33 | :param camera_name: str: 34 | The name of the camera being used. 35 | :param alpha_arr: array of floats: 36 | An array of alpha start-stop values in the tilt acquisition. 37 | :param sampling: str (optional; default is '1k'): 38 | Photo resolution, one of: 39 | - '4k' for 4k images (4096 x 4096; sampling=1) 40 | - '2k' for 2k images (2048 x 2048; sampling=2) 41 | - '1k' for 1k images (1024 x 1024; sampling=3) 42 | - '0.5k' for 05.k images (512 x 512; sampling=8) 43 | :param integration_time: float (optional; default is 3): 44 | Total exposure time for a single image in seconds. 45 | """ 46 | self.alpha_step = alpha_arr[1] - alpha_arr[0] 47 | self.camera_name = camera_name 48 | self.alpha_arr = alpha_arr 49 | self.integration_time = integration_time 50 | self.sampling = sampling 51 | self.alphas = alpha_arr[0:-1] + self.alpha_step / 2 52 | time_tilting = len(self.alphas) * self.integration_time # s 53 | distance_tilting = abs(self.alpha_arr[-1] - self.alpha_arr[0]) # deg 54 | tilt_velocity = distance_tilting / time_tilting # deg / s 55 | self.tilt_speed = tem_tilt_speed(tilt_velocity) 56 | 57 | def __str__(self): 58 | return "-- Acquisition Properties -- " \ 59 | "\nName of the camera being used: " + str(self.camera_name) + \ 60 | "\nAn array of the tilt acquisition's alpha start-stop values: " + str(self.alpha_arr) + \ 61 | "\nThe middle alpha values of the tilt acquisition: " + str(self.alphas) + \ 62 | "\nTotal exposure time for a single image: " + str(round(self.integration_time, 4)) + " [seconds]" + \ 63 | "\nPhoto resolution: " + str(self.sampling) + \ 64 | "\nAlpha tilt speed: " + str(round(self.tilt_speed, 4)) + " [Thermo Fisher speed units]" 65 | 66 | 67 | if __name__ == "__main__": 68 | 69 | import numpy as np 70 | 71 | start_alpha = -35 72 | stop_alpha = 30 73 | step_alpha = 1 74 | num_alpha = int((stop_alpha - start_alpha) / step_alpha + 1) 75 | alpha_arr_ = np.linspace(start=start_alpha, stop=stop_alpha, num=num_alpha, endpoint=True) 76 | 77 | acq_prop = AcquisitionSeriesProperties(camera_name="BM-Ceta", alpha_arr=alpha_arr_, integration_time=3, 78 | sampling='1k') 79 | 80 | print(acq_prop) 81 | -------------------------------------------------------------------------------- /pyTEM_scripts/test/micro_ed/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/test/micro_ed/__init__.py -------------------------------------------------------------------------------- /pyTEM_scripts/test/micro_ed/counting_optima.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.signal import argrelextrema 3 | 4 | shifts = np.random.random(12) 5 | 6 | local_maxima = argrelextrema(shifts, np.greater)[0] 7 | local_minima = argrelextrema(shifts, np.less)[0] 8 | 9 | print(type(local_minima[0])) 10 | 11 | print(local_minima) 12 | 13 | print(type(local_maxima)) 14 | print(type(local_minima)) 15 | 16 | optima = np.concatenate((local_minima, local_maxima)) 17 | print(optima) 18 | -------------------------------------------------------------------------------- /pyTEM_scripts/test/micro_ed/perform_tilt_series.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | import time 7 | 8 | import numpy as np 9 | from threading import Thread 10 | 11 | from pyTEM.Interface import Interface 12 | from pyTEM.lib.AcquisitionSeries import AcquisitionSeries 13 | from pyTEM_scripts.test.micro_ed.AcquisitionSeriesProperties import AcquisitionSeriesProperties 14 | 15 | 16 | def perform_tilt_series(microscope: Interface, acquisition_properties: AcquisitionSeriesProperties, 17 | shifts: np.array, verbose: bool) -> AcquisitionSeries: 18 | """ 19 | Test performing a tilt-and-acquire series. 20 | 21 | :param microscope: pyTEM.Interface: 22 | A pyTEM interface to the microscope. 23 | :param acquisition_properties: AcquisitionSeriesProperties: 24 | The acquisition properties. 25 | :param shifts: np.array of float tuples: 26 | An array of tuples of the form (x, y) where x and y are the required image shifts (in microns) required to 27 | align the image at the corresponding tilt. 28 | :param verbose: bool: 29 | Print out extra information. Useful for debugging. 30 | 31 | :return: AcquisitionSeries. 32 | The results of the tilt series acquisition. 33 | """ 34 | # Make sure the column valve is open nad the screen is removed. 35 | column_valve_position = microscope.get_column_valve_position() 36 | if column_valve_position == "closed": 37 | microscope.open_column_valve() 38 | screen_position = microscope.get_screen_position() 39 | if screen_position == "inserted": 40 | microscope.retract_screen() 41 | 42 | acq_stack = AcquisitionSeries() 43 | 44 | if verbose: 45 | print("Tilting to the start position.") 46 | microscope.set_stage_position(alpha=acquisition_properties.alpha_arr[0]) # Move to the start angle 47 | 48 | # Fill in the image stack array 49 | for i, alpha in enumerate(acquisition_properties.alphas): 50 | # So that both tilting and acquiring can happen simultaneously, tilt in a separate thread 51 | tilting_thread = TiltingThread(microscope=microscope, destination=acquisition_properties.alpha_arr[i + 1], 52 | integration_time=acquisition_properties.integration_time, 53 | speed=acquisition_properties.tilt_speed, verbose=verbose) 54 | 55 | # Apply the appropriate shift 56 | microscope.set_image_shift(x=shifts[i][0], y=shifts[i][1]) 57 | 58 | tilting_thread.start() # Start tilting 59 | 60 | # The acquisition must be performed here in the main thread where the COM interface was marshalled. 61 | overall_acq_start_time = time.time() 62 | acq, (core_acq_start, core_acq_end) = microscope.acquisition( 63 | camera_name=acquisition_properties.camera_name, sampling=acquisition_properties.sampling, 64 | exposure_time=acquisition_properties.integration_time) 65 | overall_acq_end_time = time.time() 66 | 67 | # Okay, wait for the tilting thread to return, and then we are done this one. 68 | tilting_thread.join() 69 | 70 | if verbose: 71 | print("\nCore acquisition started at: " + str(core_acq_start)) 72 | print("Core acquisition returned at: " + str(core_acq_end)) 73 | print("Core acquisition time: " + str(core_acq_end - core_acq_start)) 74 | 75 | print("\nOverall acquisition() method call started at: " + str(overall_acq_start_time)) 76 | print("Overall acquisition() method returned at: " + str(overall_acq_end_time)) 77 | print("Total overall time spent in acquisition(): " + str(overall_acq_end_time - overall_acq_start_time)) 78 | 79 | acq_stack.append(acq) 80 | 81 | # Housekeeping: Restore the column value and screen to the positions they were in before we started. 82 | if column_valve_position == "closed": 83 | microscope.close_column_valve() 84 | if screen_position == "inserted": 85 | microscope.insert_screen() 86 | 87 | return acq_stack 88 | 89 | 90 | class TiltingThread(Thread): 91 | 92 | def __init__(self, microscope: Interface, destination: float, integration_time: float, speed: float, 93 | verbose: bool = False): 94 | """ 95 | :param microscope: pyTEM.Interface: 96 | A pyTEM interface to the microscope. 97 | :param destination: float: 98 | The alpha angle to which we will tilt. 99 | :param integration_time: float: 100 | The requested exposure time for the tilt image, in seconds. 101 | :param speed: float: 102 | Tilt speed, in TEM fractional tilt speed units. 103 | :param verbose: bool: 104 | Print out extra information. Useful for debugging. 105 | """ 106 | Thread.__init__(self) 107 | self.microscope = microscope 108 | self.destination = destination 109 | self.speed = speed 110 | self.integration_time = integration_time 111 | self.verbose = verbose 112 | 113 | def run(self): 114 | # We need to give the acquisition thread a bit of a head start before we start tilting 115 | time.sleep(0.355 + self.integration_time) 116 | 117 | start_time = time.time() 118 | self.microscope.set_stage_position_alpha(alpha=self.destination, speed=self.speed, movement_type="go") 119 | stop_time = time.time() 120 | 121 | if self.verbose: 122 | print("\nStarting tilting at: " + str(start_time)) 123 | print("Stopping tilting at: " + str(stop_time)) 124 | print("Total time spent tilting: " + str(stop_time - start_time)) 125 | 126 | 127 | if __name__ == "__main__": 128 | """ 129 | Testing 130 | """ 131 | 132 | try: 133 | scope = Interface() 134 | except BaseException as e: 135 | print(e) 136 | print("Unable to connect to microscope, proceeding with None object.") 137 | scope = None 138 | 139 | out_file = "C:/Users/Supervisor.TALOS-9950969/Downloads/test_series" 140 | 141 | start_alpha_ = -20 142 | stop_alpha_ = 20 143 | step_alpha_ = 1 144 | 145 | num_alpha = int((stop_alpha_ - start_alpha_) / step_alpha_ + 1) 146 | alpha_arr = np.linspace(start=start_alpha_, stop=stop_alpha_, num=num_alpha, endpoint=True) 147 | 148 | print("alpha_arr:") 149 | print(alpha_arr) 150 | 151 | aqc_props = AcquisitionSeriesProperties(camera_name='BM-Ceta', alpha_arr=alpha_arr, 152 | integration_time=0.5, sampling='1k') 153 | 154 | shifts_ = np.full(shape=len(aqc_props.alphas), dtype=(float, 2), fill_value=0.0) 155 | 156 | print("Shifts:") 157 | print(shifts_) 158 | 159 | acq_series = perform_tilt_series(microscope=scope, acquisition_properties=aqc_props, shifts=shifts_, verbose=True) 160 | 161 | # Save the image series to file. 162 | print("Saving image series to file as: " + out_file + ".mrc") 163 | acq_series.save_as_mrc(out_file + ".mrc") 164 | 165 | # Also, save each image individually as a jpeg for easy viewing. 166 | for counter, acquisition in enumerate(acq_series): 167 | out_file_temp = out_file + "_" + str(counter) + ".jpeg" 168 | print("Saving image #" + str(counter) + "to file as: " + out_file_temp) 169 | acquisition.save_to_file(out_file=out_file_temp, extension=".jpeg") 170 | -------------------------------------------------------------------------------- /pyTEM_scripts/test/micro_ed/tilt_acq_timing/acq_timing.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/test/micro_ed/tilt_acq_timing/acq_timing.txt -------------------------------------------------------------------------------- /pyTEM_scripts/test/micro_ed/tilt_speed_calibration/tilt_speed.txt: -------------------------------------------------------------------------------- 1 | 40 deg with tilt speed of 1: 2 | 2.6992154121398926 3 | 2.6802895069122314 4 | 2.689208507537842 5 | 6 | Assume: 2.69 s 7 | 8 | 9 | 40 deg with tilt speed of 0.5: 10 | 3.7720274925231934 11 | 3.782043933868408 12 | 3.9204485416412354 13 | 3.820314884185791 14 | 15 | Assume: 3.80 s 16 | 17 | 18 | 40 deg with tilt speed of 0.25: 19 | 6.361107349395752 20 | 6.392581462860107 21 | 6.381195068359375 22 | 23 | Assume: 6.38 s 24 | 25 | 26 | 40 deg with tilt speed of 0.1: 27 | 14.296462774276733 28 | 14.309964895248413 29 | 14.319045066833496 30 | 31 | Assume 14.31 s 32 | 33 | 34 | 40 deg with tilt speed of 0.05: 35 | 27.66869616508484 36 | 27.674469470977783 37 | 27.68709945678711 38 | 39 | Assume: 27.68 s 40 | 41 | 42 | 40 deg with tilt speed of 0.01: 43 | 135.03190875053406 44 | 135.0221405029297 45 | 46 | Assume: 135.03 s 47 | 48 | 49 | 40 deg with tilt speed of 0.02: 50 | 51 | Assume 68 s 52 | 53 | 54 | 2 deg with tilt speed of 0.0005: 55 | 56 | Assume 134.7 s 57 | 58 | 59 | 1 deg with tilt speed of 0.00001: 60 | # Failed, this is too slow 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /pyTEM_scripts/test/micro_ed/tilt_speed_calibration/tilt_speed_calibration.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basf/pyTEM/d2b2800329524c205c266fe72a35c7fd477cc735/pyTEM_scripts/test/micro_ed/tilt_speed_calibration/tilt_speed_calibration.xlsx -------------------------------------------------------------------------------- /pyTEM_scripts/test/micro_ed/tkinter_test.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from tkinter import filedialog 4 | 5 | 6 | def hello_world(): 7 | """ 8 | 9 | """ 10 | root = tk.Tk() 11 | root.title("Willkommen!") 12 | 13 | welcome_message = "Welcome to the micro electron diffraction (MicroED) automated imaging software. " \ 14 | "MicroED allows fast, high resolution 3D structure determination of small chemical compounds " \ 15 | "biological macromolecules!" 16 | 17 | window_width = 600 18 | window_height = 300 19 | 20 | # get the screen dimension 21 | screen_width = root.winfo_screenwidth() 22 | screen_height = root.winfo_screenheight() 23 | 24 | # find the center point 25 | center_x = int(screen_width / 2 - window_width / 2) 26 | center_y = int(screen_height / 2 - window_height / 2) 27 | 28 | # set the position of the window to the center of the screen 29 | root.geometry(f'{window_width}x{window_height}+{center_x}+{center_y}') 30 | 31 | # root.iconbitmap("../.ico") 32 | 33 | message = ttk.Label(root, text=welcome_message, wraplength=window_width, font=("Arial", 25)) 34 | 35 | # tk.Button(frm, text="Quit", command=root.destroy).grid(column=1, row=0) 36 | 37 | exit_button = ttk.Button(root, text="Quit", command=lambda: root.quit()) 38 | message.pack(ipadx=5, ipady=5, expand=True) 39 | exit_button.pack(ipadx=5, ipady=5, expand=True) 40 | 41 | root.mainloop() 42 | 43 | 44 | def display_text(entry, label): 45 | string = entry.get() 46 | label.configure(text=string) 47 | 48 | 49 | def get_user_input(): 50 | # Create an instance of Tkinter frame 51 | root = tk.Tk() 52 | 53 | # Initialize a Label to display the User Input 54 | label = ttk.Label(root, text="", font=("Courier 22 bold")) 55 | label.pack() 56 | 57 | # Create an Entry widget to accept User Input 58 | entry = ttk.Entry(root, width=40) 59 | entry.focus_set() 60 | entry.pack() 61 | 62 | # Create a Button to validate Entry Widget 63 | ttk.Button(root, text="Okay", width=20, command=lambda: display_text(entry, label)).pack(pady=20) 64 | 65 | root.mainloop() 66 | 67 | 68 | def get_save_directory(): 69 | 70 | root = tk.Tk() 71 | 72 | # root.filename = filedialog.asksaveasfilename(initialdir="/", title="Select out directory", 73 | # filetypes=(("jpeg files", "*.jpg"), ("all files", "*.*"))) 74 | # print(root.filename) 75 | 76 | root.directory = filedialog.askdirectory() 77 | 78 | print(root.directory) 79 | 80 | 81 | if __name__ == "__main__": 82 | # hello_world() 83 | # get_user_input() 84 | 85 | get_save_directory() 86 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy~=1.21.6 2 | pandas~=1.3.4 3 | hyperspy~=1.7.0 4 | scipy~=1.8.1 5 | tifffile~=2022.5.4 6 | setuptools~=60.2.0 7 | comtypes~=1.1.11 8 | matplotlib~=3.5.2 9 | mrcfile~=1.3.0 10 | pathlib~=1.0.1 11 | Pillow~=9.1.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Michael Luciuk 3 | Date: Summer 2022 4 | """ 5 | 6 | from setuptools import setup, find_packages 7 | from pathlib import Path 8 | 9 | this_directory = Path(__file__).parent 10 | long_description = (this_directory / "README.md").read_text() 11 | 12 | setup( 13 | name='pyTEM', 14 | version='0.1.0', 15 | description='Control and Automation of Transmission Electron Microscopes', 16 | long_description=long_description, 17 | url='https://github.com/basf/pyTEM', 18 | author='BASF TEM Laboratory', 19 | author_email='philipp.mueller@basf.com', 20 | keywords=['transmission electron microscopy', 'TEM', 'microscopy', 'electron microscopy', 21 | 'micro-crystal electron diffraction', 'uED'], 22 | license='MIT', 23 | packages=find_packages(exclude=['*.test', '*.test.*', 'test.*', 'test']), 24 | python_requires='>=3.8', 25 | install_requires=['numpy', 26 | 'pandas', 27 | 'hyperspy', 28 | 'scipy', 29 | 'tifffile', 30 | 'comtypes', 31 | 'matplotlib', 32 | 'setuptools', 33 | 'mrcfile', 34 | 'pathlib', 35 | 'Pillow', 36 | ], 37 | entry_points=''' 38 | [console_scripts] 39 | align_images=pyTEM_scripts.align_images:script_entry 40 | bulk_carbon_analysis=pyTEM_scripts.bulk_carbon_analysis:script_entry 41 | micro_ed=pyTEM_scripts.micro_ed:script_entry 42 | ''', 43 | package_data={'': ['*.ico', '*.npy']}, 44 | include_package_data=True, 45 | ) 46 | --------------------------------------------------------------------------------