Coverage for src\pytribeam\fib.py: 82%
120 statements
« prev ^ index » next coverage.py v7.5.1, created at 2025-03-04 17:41 -0800
« prev ^ index » next coverage.py v7.5.1, created at 2025-03-04 17:41 -0800
1#!/usr/bin/python3
3# Default python modules
4from functools import singledispatch
5import os
6from pathlib import Path
7import time
8import warnings
9import math
10from typing import NamedTuple, List, Tuple
11from functools import singledispatch
12import subprocess
14# Autoscript included modules
15from PIL import Image as pil_img
16import cv2
17import numpy as np
18from matplotlib import pyplot as plt
19import h5py
21# 3rd party module
23# Local scripts
24import pytribeam.constants as cs
25from pytribeam.constants import Conversions
26import pytribeam.insertable_devices as devices
27import pytribeam.factory as factory
28import pytribeam.types as tbt
29import pytribeam.utilities as ut
30import pytribeam.image as img
33def application_files(microscope: tbt.Microscope) -> List[str]:
34 """Gets application file list from the current microscope"""
35 apps = microscope.patterning.list_all_application_files()
37 # Remove "None" entry
38 while "None" in apps:
39 apps.remove("None")
40 apps.sort(key=str.casefold)
42 return apps
45def shutter_control(microscope: tbt.Microscope) -> None:
46 """Ensure auto control set on e-beam shutter. Manual control not currently offered."""
47 shutter = microscope.beams.electron_beam.protective_shutter
48 if not shutter.is_installed:
49 warnings.warn("Protective E-beam shutter not installed on this system.")
50 return
51 status = shutter.mode.value
52 if status != tbt.ProtectiveShutterMode.AUTOMATIC:
53 shutter.mode.value = tbt.ProtectiveShutterMode.AUTOMATIC
54 new_status = shutter.mode.value
55 if new_status != tbt.ProtectiveShutterMode.AUTOMATIC:
56 raise SystemError(
57 "E-beam shutter for FIB milling is installed but cannot set control to 'Automatic' mode."
58 )
59 warnings.warn(
60 "E-beam shutter for FIB milling operations is in auto-mode, which may not insert at certain tilt angles and stage positions. Manual control not available."
61 )
62 return
65def prepare_milling(
66 microscope: tbt.Microscope,
67 application: str,
68 patterning_device: tbt.Device = tbt.Device.ION_BEAM,
69) -> bool:
70 # TODO validation and error checking from TFS
71 # TODO support e-beam patterning via the mill_beam settings
72 """Clears old patterns, assigns patterning to ion beam by default, and loads application. No way to currently verify values have been set properly but can validate application by trying to make a pattern."""
73 valid_devices = [tbt.Device.ELECTRON_BEAM, tbt.Device.ION_BEAM]
74 if patterning_device not in valid_devices:
75 raise ValueError(
76 f"Invalid patterning device of '{patterning_device}' requested, only '{[i for i in valid_devices]}' are valid."
77 )
78 microscope.patterning.clear_patterns()
79 microscope.patterning.set_default_beam_type(beam_index=patterning_device.value)
80 if application not in microscope.patterning.list_all_application_files():
81 raise ValueError(
82 f"Invalid application file on this system, there is no patterning application with name: '{application}'."
83 )
84 else:
85 microscope.patterning.set_default_application_file(application)
87 return True
90@singledispatch
91def create_pattern(
92 geometry,
93 microscope: tbt.Microscope,
94 **kwargs: dict,
95) -> bool: # FIBRectanglePattern
96 """"""
97 _ = geometry
98 __ = microscope
99 ___ = kwargs
100 raise NotImplementedError(f"No handler for type {type(geometry)}")
103@create_pattern.register
104def _(
105 geometry: tbt.FIBRectanglePattern,
106 microscope: tbt.Microscope,
107 **kwargs: dict,
108) -> tbt.as_dynamics.RectanglePattern:
109 pattern = microscope.patterning.create_rectangle(
110 center_x=geometry.center_um.x * Conversions.UM_TO_M,
111 center_y=geometry.center_um.y * Conversions.UM_TO_M,
112 width=geometry.width_um * Conversions.UM_TO_M,
113 height=geometry.height_um * Conversions.UM_TO_M,
114 depth=geometry.depth_um * Conversions.UM_TO_M,
115 )
116 pattern.scan_direction = geometry.scan_direction.value
117 pattern.scan_type = geometry.scan_type.value
119 return pattern
122@create_pattern.register
123def _(
124 geometry: tbt.FIBRegularCrossSection,
125 microscope: tbt.Microscope,
126 **kwargs: dict,
127) -> tbt.as_dynamics.RegularCrossSectionPattern:
128 pattern = microscope.patterning.create_regular_cross_section(
129 center_x=geometry.center_um.x * Conversions.UM_TO_M,
130 center_y=geometry.center_um.y * Conversions.UM_TO_M,
131 width=geometry.width_um * Conversions.UM_TO_M,
132 height=geometry.height_um * Conversions.UM_TO_M,
133 depth=geometry.depth_um * Conversions.UM_TO_M,
134 )
135 pattern.scan_direction = geometry.scan_direction.value
136 pattern.scan_type = geometry.scan_type.value
138 return pattern
141@create_pattern.register
142def _(
143 geometry: tbt.FIBCleaningCrossSection,
144 microscope: tbt.Microscope,
145 **kwargs: dict,
146) -> tbt.as_dynamics.CleaningCrossSectionPattern:
147 pattern = microscope.patterning.create_cleaning_cross_section(
148 center_x=geometry.center_um.x * Conversions.UM_TO_M,
149 center_y=geometry.center_um.y * Conversions.UM_TO_M,
150 width=geometry.width_um * Conversions.UM_TO_M,
151 height=geometry.height_um * Conversions.UM_TO_M,
152 depth=geometry.depth_um * Conversions.UM_TO_M,
153 )
154 pattern.scan_direction = geometry.scan_direction.value
155 pattern.scan_type = geometry.scan_type.value
157 return pattern
160@create_pattern.register
161def _(
162 geometry: tbt.FIBStreamPattern,
163 microscope: tbt.Microscope,
164 **kwargs: dict,
165) -> tbt.StreamPattern:
166 """Stream patterns are only supported at 16 bit depth now, but this is generally too many points, so we upscale images as if we were creting a 12 bit stream file and correct the offset mathematically."""
168 # run image_processing and check that mask file is created
169 input_image_path = kwargs["kwargs"]["input_image_path"]
170 image_processing(
171 geometry=geometry,
172 input_image_path=input_image_path,
173 )
175 # get mask
176 mask_path = geometry.mask_file
177 with pil_img.open(mask_path) as mask_img:
178 width, height = mask_img.size
179 mask = np.asarray(mask_img).astype(int)
181 stream_def = tbt.StreamPatternDefinition()
182 stream_def.bit_depth = tbt.StreamDepth.BITS_16 # only supported bit depth now
183 dwell_time = geometry.dwell_us * Conversions.US_TO_S
185 scale_factor = cs.Constants.stream_pattern_scale / width
186 point_img = cv2.resize(
187 mask,
188 dsize=(int(width * scale_factor), int(height * scale_factor)),
189 interpolation=cv2.INTER_NEAREST,
190 )
191 idx = np.where(point_img == 1)
192 num_points = (
193 len(idx[0]) + 2
194 ) # top right and bottom left corners each have a point to make sure pattern is centered
196 # stream pattern is defined by 4 values for each point
197 # [x, y, dwell_time, flags]
198 # flags = 1 --> blank beam
199 # flags = 0 --> use beam
200 stream_def.points = np.zeros(shape=(num_points, 4), dtype=object)
201 stream_def.points[0] = [
202 1, # x (top left)
203 cs.Constants.stream_pattern_y_shift, # y (top left)
204 dwell_time, # dwell
205 1, # flag (1 means blank the beam)
206 ]
207 flags = 0
208 for i in range(1, num_points - 1): # start at first point
209 x = idx[1][i - 1] * 16 + 1
210 y = (
211 idx[0][i - 1] * 16 + cs.Constants.stream_pattern_y_shift
212 ) # + (0.17 * (2**stream_def.bit_depth))
213 stream_def.points[i] = [x, y, dwell_time, flags]
214 stream_def.points[-1] = [
215 2**stream_def.bit_depth, # x (bottom right)
216 2**stream_def.bit_depth
217 - cs.Constants.stream_pattern_y_shift, # y (bottom right)
218 dwell_time, # dwell
219 1, # flag
220 ]
222 stream_def.repeat_count = geometry.repeats
224 stream_pattern = microscope.patterning.create_stream(
225 center_x=0.0,
226 center_y=0.0,
227 stream_pattern_definition=stream_def,
228 )
230 return stream_pattern
233def image_processing(
234 geometry: tbt.FIBStreamPattern,
235 input_image_path: Path,
236) -> bool:
237 output = subprocess.run(
238 [
239 "python",
240 (geometry.recipe_file).as_posix(),
241 input_image_path.as_posix(), # input path,
242 (geometry.mask_file).as_posix(),
243 # outputpath,
244 ],
245 capture_output=True,
246 )
247 if output.returncode != 0:
248 raise ValueError(
249 f"Subprocess call for script {geometry.recipe_file} using executable 'python' did not execute correctly."
250 )
251 # check for mask file
252 if not geometry.mask_file.exists():
253 raise ValueError(
254 f"Mask file at location {geometry.mask_file} should have been created by image processing recipe but does not exist. Please check your recipe_file script."
255 )
256 # TODO
257 # save masks
259 return True
262# TODO add more complex patterning behavior
263def mill_operation(
264 step: tbt.Step,
265 fib_settings: tbt.FIBSettings,
266 general_settings: tbt.GeneralSettings,
267 slice_number: int,
268) -> bool:
269 microscope = fib_settings.microscope
271 shutter_control(microscope=microscope)
272 # prepare beam
273 img.imaging_device(microscope=microscope, beam=fib_settings.mill_beam)
274 # set milling application and device
276 prepare_milling(
277 microscope=microscope,
278 application=fib_settings.pattern.application,
279 )
281 # get expected path of the fib image
282 if fib_settings.pattern.type != tbt.FIBPatternType.SELECTED_AREA:
283 input_image_path = None
284 else:
285 input_image_path = Path.join(
286 general_settings.exp_dir,
287 step.name,
288 f"{slice_number:04}.tif",
289 )
290 if not input_image_path.exists():
291 raise ValueError(
292 f"Ion image for selected area milling was not found at '{input_image_path}'."
293 )
295 # make the pattern
296 pattern = create_pattern(
297 fib_settings.pattern.geometry,
298 microscope=microscope,
299 kwargs={"input_image_path": input_image_path},
300 )
302 microscope.patterning.run()
304 return True