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

1#!/usr/bin/python3 

2 

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 

13 

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 

20 

21# 3rd party module 

22 

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 

31 

32 

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() 

36 

37 # Remove "None" entry 

38 while "None" in apps: 

39 apps.remove("None") 

40 apps.sort(key=str.casefold) 

41 

42 return apps 

43 

44 

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 

63 

64 

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) 

86 

87 return True 

88 

89 

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)}") 

101 

102 

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 

118 

119 return pattern 

120 

121 

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 

137 

138 return pattern 

139 

140 

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 

156 

157 return pattern 

158 

159 

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.""" 

167 

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 ) 

174 

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) 

180 

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 

184 

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 

195 

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 ] 

221 

222 stream_def.repeat_count = geometry.repeats 

223 

224 stream_pattern = microscope.patterning.create_stream( 

225 center_x=0.0, 

226 center_y=0.0, 

227 stream_pattern_definition=stream_def, 

228 ) 

229 

230 return stream_pattern 

231 

232 

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 

258 

259 return True 

260 

261 

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 

270 

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 

275 

276 prepare_milling( 

277 microscope=microscope, 

278 application=fib_settings.pattern.application, 

279 ) 

280 

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 ) 

294 

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 ) 

301 

302 microscope.patterning.run() 

303 

304 return True