├── .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 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------