Coverage for src/pytribeam/fib.py: 80%

107 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2026-09-09 18:27 +0000

1#!/usr/bin/python3 

2""" 

3Focused ion beam patterning and milling operations. 

4 

5This module provides the high-level utilities used to prepare, create, and run 

6FIB milling patterns on a microscope. It includes helpers for loading patterning 

7applications, configuring the microscope patterning system, creating microscope 

8patterns from `pytribeam` geometry objects, and executing a milling operation 

9for a single slice. 

10 

11Most users should interact with this module through `mill_operation`, which 

12coordinates shutter setup, beam selection, patterning application setup, pattern 

13creation, and execution. Lower-level functions such as `prepare_milling` and 

14`create_pattern` are available for workflows that need finer control. 

15 

16## Typical workflow 

17 

18from pytribeam.fib import mill_operation 

19 

20mill_operation( 

21 step=step, 

22 fib_settings=fib_settings, 

23 general_settings=general_settings, 

24 slice_number=0, 

25) 

26 

27For workflows that need to prepare the microscope and create a pattern manually: 

28 

29 

30from pytribeam.fib import prepare_milling, create_pattern 

31 

32prepare_milling( 

33 microscope=microscope, 

34 application="Si-ccs", 

35) 

36 

37pattern = create_pattern( 

38 geometry=fib_settings.pattern.geometry, 

39 microscope=microscope, 

40) 

41 

42 

43## Main entry points 

44 

45- `shutter_control`: ensure the electron-beam protective shutter is in automatic 

46 mode before milling. 

47- `prepare_milling`: clear existing patterns, select the patterning beam, and 

48 load a patterning application. 

49- `create_pattern`: create a microscope pattern from a supported FIB geometry 

50 object. 

51- `image_processing`: run a recipe script to generate a mask for stream-pattern 

52 milling. 

53- `mill_operation`: perform the complete milling operation for one step and 

54 slice. 

55 

56## Supported pattern geometries 

57 

58`create_pattern` is implemented as a `functools.singledispatch` function. The 

59currently supported geometry types are: 

60 

61| Geometry type | Created microscope pattern | 

62| --- | --- | 

63| `tbt.FIBRectanglePattern` | Rectangle pattern | 

64| `tbt.FIBRegularCrossSection` | Regular cross-section pattern | 

65| `tbt.FIBCleaningCrossSection` | Cleaning cross-section pattern | 

66| `tbt.FIBStreamPattern` | Stream pattern generated from an image mask | 

67 

68Unsupported geometry types raise `NotImplementedError`. 

69 

70## Units 

71 

72User-facing FIB geometry values are expected to be in micrometers or 

73microseconds according to the field name, for example `width_um`, `depth_um`, 

74and `dwell_us`. Values are converted to microscope API units internally using 

75`pytribeam.constants.Conversions`. 

76 

77## Stream-pattern image processing 

78 

79Stream patterns use an external image-processing recipe to convert an input 

80image into a mask. The recipe is executed as a Python subprocess using 

81`geometry.recipe_file`, and the generated mask is expected at 

82`geometry.mask_file`. 

83 

84The mask image is converted into a stream-pattern point list, where active mask 

85pixels define beam-on points and the first and last points are used to define 

86the pattern extents. 

87 

88> **Warning** 

89> 

90> Functions in this module can modify microscope state and may initiate 

91> milling. Use them only when the microscope, beam conditions, stage position, 

92> patterning application, and sample state have been verified. 

93 

94<hr style="height: 12px; background-color: #333; border: none;"> 

95""" 

96 

97__all__ = [ 

98 "shutter_control", 

99 "prepare_milling", 

100 "create_pattern", 

101 "image_processing", 

102 "mill_operation", 

103] 

104 

105# Default python modules 

106import sys 

107from functools import singledispatch 

108from pathlib import Path 

109import warnings 

110from typing import List 

111from functools import singledispatch 

112import subprocess 

113 

114# Autoscript included modules 

115from PIL import Image as pil_img 

116import cv2 

117import numpy as np 

118 

119# 3rd party module 

120 

121# Local scripts 

122import pytribeam.constants as cs 

123from pytribeam.constants import Conversions 

124import pytribeam.types as tbt 

125import pytribeam.image as img 

126 

127 

128def shutter_control(microscope: tbt.Microscope) -> None: 

129 """ 

130 Ensure auto control is set on the e-beam shutter. Manual control is not currently offered. 

131 

132 This function checks if the e-beam protective shutter is installed and sets its mode to automatic if it is not already set. If the shutter cannot be set to automatic mode, a SystemError is raised. 

133 

134 ## Parameters 

135 

136 - `microscope` (`tbt.Microscope`): The microscope object for which to control the e-beam shutter. 

137 

138 ## Raises 

139 

140 - `SystemError`: If the e-beam shutter is installed but cannot be set to automatic mode. 

141 

142 ## Warnings 

143 

144 - `UserWarning`: If the e-beam shutter is not installed or if it is set to automatic mode. 

145 """ 

146 shutter = microscope.beams.electron_beam.protective_shutter 

147 if not shutter.is_installed: 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true

148 warnings.warn("Protective E-beam shutter not installed on this system.") 

149 return 

150 status = shutter.mode.value 

151 if status != tbt.ProtectiveShutterMode.AUTOMATIC: 151 ↛ 153line 151 didn't jump to line 153 because the condition on line 151 was always true

152 shutter.mode.value = tbt.ProtectiveShutterMode.AUTOMATIC 

153 new_status = shutter.mode.value 

154 if new_status != tbt.ProtectiveShutterMode.AUTOMATIC: 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true

155 raise SystemError( 

156 "E-beam shutter for FIB milling is installed but cannot set control to 'Automatic' mode." 

157 ) 

158 warnings.warn( 

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

160 ) 

161 return 

162 

163 

164def prepare_milling( 

165 microscope: tbt.Microscope, 

166 application: str, 

167 patterning_device: tbt.Device = tbt.Device.ION_BEAM, 

168) -> bool: 

169 # TODO validation and error checking from TFS 

170 # TODO support e-beam patterning via the mill_beam settings 

171 """ 

172 Clear old patterns, assign patterning to ion beam by default, and load the application. 

173 

174 This function clears old patterns, assigns the patterning device to the specified beam (ion beam by default), and loads the specified application. It validates the patterning device and application file. 

175 

176 ## Parameters 

177 

178 - `microscope` (`tbt.Microscope`): The microscope object for which to prepare milling. 

179 - `application` (`str`): The name of the application file to load. 

180 - `patterning_device` (`tbt.Device, optional`): The device to use for patterning (default is tbt.Device.ION_BEAM). 

181 

182 ## Returns 

183 

184 - `bool`: True if the preparation is successful. 

185 

186 ## Raises 

187 

188 - `ValueError`: If the patterning device is invalid or if the application file is not found on the system. 

189 """ 

190 valid_devices = [tbt.Device.ELECTRON_BEAM, tbt.Device.ION_BEAM] 

191 if patterning_device not in valid_devices: 

192 raise ValueError( 192 ↛ exit,   192 ↛ exit2 missed branches: 1) line 192 didn't jump to the function exit, 2) line 192 didn't except from function 'prepare_milling' because the raise on line 192 wasn't executed

193 f"Invalid patterning device of '{patterning_device}' requested, only '{[i for i in valid_devices]}' are valid." 

194 ) 

195 microscope.patterning.clear_patterns() 

196 microscope.patterning.set_default_beam_type(beam_index=patterning_device.value) 

197 if application not in microscope.patterning.list_all_application_files(): 

198 raise ValueError( 

199 f"Invalid application file on this system, there is no patterning application with name: '{application}'." 

200 ) 

201 else: 

202 microscope.patterning.set_default_application_file(application) 

203 

204 return True 

205 

206 

207@singledispatch 

208def create_pattern( 

209 geometry, 

210 microscope: tbt.Microscope, 

211 **kwargs: dict, 

212) -> bool: # FIBRectanglePattern 

213 """ 

214 Create a pattern on the microscope based on the provided geometry. 

215 

216 This function creates a pattern on the microscope based on the provided geometry. It is a generic function that raises a NotImplementedError if no handler is available for the provided geometry type. 

217 

218 ## Parameters 

219 

220 - `geometry` (`Any`): The geometry of the pattern to create. 

221 - `microscope` (`tbt.Microscope`): The microscope object on which to create the pattern. 

222 - `kwargs` (`dict`): Additional keyword arguments. 

223 

224 ## Returns 

225 

226 - `bool`: True if the pattern is created successfully. 

227 

228 ## Raises 

229 

230 - `NotImplementedError`: If no handler is available for the provided geometry type. 

231 """ 

232 _ = geometry 

233 __ = microscope 

234 ___ = kwargs 

235 raise NotImplementedError(f"No handler for type {type(geometry)}") 

236 

237 

238@create_pattern.register 

239def _create_rectangle_pattern( 

240 geometry: tbt.FIBRectanglePattern, 

241 microscope: tbt.Microscope, 

242 **kwargs: dict, 

243) -> tbt.as_dynamics.RectanglePattern: 

244 """ 

245 Create a rectangle pattern on the microscope. 

246 

247 ## Parameters 

248 

249 - `geometry` (`tbt.FIBRectanglePattern`): The geometry of the rectangle pattern to create. 

250 - `microscope` (`tbt.Microscope`): The microscope object on which to create the pattern. 

251 - `kwargs` (`dict`): Additional keyword arguments. 

252 

253 ## Returns 

254 

255 - `tbt.as_dynamics.RectanglePattern`: The created rectangle pattern. 

256 """ 

257 pattern = microscope.patterning.create_rectangle( 

258 center_x=geometry.center_um.x * Conversions.UM_TO_M, 

259 center_y=geometry.center_um.y * Conversions.UM_TO_M, 

260 width=geometry.width_um * Conversions.UM_TO_M, 

261 height=geometry.height_um * Conversions.UM_TO_M, 

262 depth=geometry.depth_um * Conversions.UM_TO_M, 

263 ) 

264 pattern.scan_direction = geometry.scan_direction.value 

265 pattern.scan_type = geometry.scan_type.value 

266 

267 return pattern 

268 

269 

270@create_pattern.register 

271def _create_regular_cs_pattern( 

272 geometry: tbt.FIBRegularCrossSection, 

273 microscope: tbt.Microscope, 

274 **kwargs: dict, 

275) -> tbt.as_dynamics.RegularCrossSectionPattern: 

276 """ 

277 Create a regular cross-section pattern on the microscope. 

278 

279 ## Parameters 

280 

281 - `geometry` (`tbt.FIBRegularCrossSection`): The geometry of the regular cross-section pattern to create. 

282 - `microscope` (`tbt.Microscope`): The microscope object on which to create the pattern. 

283 - `kwargs` (`dict`): Additional keyword arguments. 

284 

285 ## Returns 

286 

287 - `tbt.as_dynamics.RegularCrossSectionPattern`: The created regular cross-section pattern. 

288 """ 

289 pattern = microscope.patterning.create_regular_cross_section( 

290 center_x=geometry.center_um.x * Conversions.UM_TO_M, 

291 center_y=geometry.center_um.y * Conversions.UM_TO_M, 

292 width=geometry.width_um * Conversions.UM_TO_M, 

293 height=geometry.height_um * Conversions.UM_TO_M, 

294 depth=geometry.depth_um * Conversions.UM_TO_M, 

295 ) 

296 pattern.scan_direction = geometry.scan_direction.value 

297 pattern.scan_type = geometry.scan_type.value 

298 

299 return pattern 

300 

301 

302@create_pattern.register 

303def _create_cleaning_cs_pattern( 

304 geometry: tbt.FIBCleaningCrossSection, 

305 microscope: tbt.Microscope, 

306 **kwargs: dict, 

307) -> tbt.as_dynamics.CleaningCrossSectionPattern: 

308 """ 

309 Create a cleaning cross-section pattern on the microscope. 

310 

311 ## Parameters 

312 

313 - `geometry` (`tbt.FIBCleaningCrossSection`): The geometry of the cleaning cross-section pattern to create. 

314 - `microscope` (`tbt.Microscope`): The microscope object on which to create the pattern. 

315 - `kwargs` (`dict`): Additional keyword arguments. 

316 

317 ## Returns 

318 

319 - `tbt.as_dynamics.CleaningCrossSectionPattern`: The created cleaning cross-section pattern. 

320 """ 

321 pattern = microscope.patterning.create_cleaning_cross_section( 

322 center_x=geometry.center_um.x * Conversions.UM_TO_M, 

323 center_y=geometry.center_um.y * Conversions.UM_TO_M, 

324 width=geometry.width_um * Conversions.UM_TO_M, 

325 height=geometry.height_um * Conversions.UM_TO_M, 

326 depth=geometry.depth_um * Conversions.UM_TO_M, 

327 ) 

328 pattern.scan_direction = geometry.scan_direction.value 

329 pattern.scan_type = geometry.scan_type.value 

330 

331 return pattern 

332 

333 

334@create_pattern.register 

335def _create_stream_pattern( 

336 geometry: tbt.FIBStreamPattern, 

337 microscope: tbt.Microscope, 

338 **kwargs: dict, 

339) -> tbt.StreamPattern: 

340 """ 

341 Create a stream pattern on the microscope. 

342 

343 ## Parameters 

344 

345 - `geometry` (`tbt.FIBStreamPattern`): The geometry of the stream pattern to create. 

346 - `microscope` (`tbt.Microscope`): The microscope object on which to create the pattern. 

347 - `kwargs` (`dict`): Additional keyword arguments. 

348 

349 ## Returns 

350 

351 - `tbt.StreamPattern`: The created stream pattern. 

352 """ 

353 

354 # run image_processing and check that mask file is created 

355 input_image_path = kwargs["kwargs"]["input_image_path"] 

356 image_processing( 

357 geometry=geometry, 

358 input_image_path=input_image_path, 

359 ) 

360 

361 # get mask 

362 mask_path = geometry.mask_file 

363 with pil_img.open(mask_path) as mask_img: 

364 width, height = mask_img.size 

365 mask = np.asarray(mask_img).astype(int) 

366 

367 stream_def = tbt.StreamPatternDefinition() 

368 stream_def.bit_depth = tbt.StreamDepth.BITS_16 # only supported bit depth now 

369 dwell_time = geometry.dwell_us * Conversions.US_TO_S 

370 

371 scale_factor = cs.Constants.stream_pattern_scale / width 

372 point_img = cv2.resize( 

373 mask, 

374 dsize=(int(width * scale_factor), int(height * scale_factor)), 

375 interpolation=cv2.INTER_NEAREST, 

376 ) 

377 idx = np.where(point_img == 1) 

378 num_points = ( 

379 len(idx[0]) + 2 

380 ) # top right and bottom left corners each have a point to make sure pattern is centered 

381 

382 # stream pattern is defined by 4 values for each point 

383 # [x, y, dwell_time, flags] 

384 # flags = 1 --> blank beam 

385 # flags = 0 --> use beam 

386 stream_def.points = np.zeros(shape=(num_points, 4), dtype=object) 

387 stream_def.points[0] = [ 

388 1, # x (top left) 

389 cs.Constants.stream_pattern_y_shift, # y (top left) 

390 dwell_time, # dwell 

391 1, # flag (1 means blank the beam) 

392 ] 

393 flags = 0 

394 for i in range(1, num_points - 1): # start at first point 

395 x = idx[1][i - 1] * 16 + 1 

396 y = ( 

397 idx[0][i - 1] * 16 + cs.Constants.stream_pattern_y_shift 

398 ) # + (0.17 * (2**stream_def.bit_depth)) 

399 stream_def.points[i] = [x, y, dwell_time, flags] 

400 stream_def.points[-1] = [ 

401 2**stream_def.bit_depth, # x (bottom right) 

402 2**stream_def.bit_depth 

403 - cs.Constants.stream_pattern_y_shift, # y (bottom right) 

404 dwell_time, # dwell 

405 1, # flag 

406 ] 

407 

408 stream_def.repeat_count = geometry.repeats 

409 

410 stream_pattern = microscope.patterning.create_stream( 

411 center_x=0.0, 

412 center_y=0.0, 

413 stream_pattern_definition=stream_def, 

414 ) 

415 

416 return stream_pattern 

417 

418 

419def image_processing( 

420 geometry: tbt.FIBStreamPattern, 

421 input_image_path: Path, 

422) -> bool: 

423 """ 

424 Perform image processing for FIB stream pattern. 

425 

426 This function runs an image processing script specified by the `recipe_file` in the `geometry` object, using the input image path and outputting the mask file. 

427 

428 ## Parameters 

429 

430 - `geometry` (`tbt.FIBStreamPattern`): The geometry of the FIB stream pattern, including the `recipe_file` and `mask_file`. 

431 - `input_image_path` (`Path`): The path to the input image. 

432 

433 ## Returns 

434 

435 - `bool`: True if the image processing is successful. 

436 

437 ## Raises 

438 

439 - `ValueError`: If the subprocess call for the script does not execute correctly or if the mask file is not created. 

440 """ 

441 output = subprocess.run( 

442 [ 

443 sys.executable, 

444 (geometry.recipe_file).as_posix(), # recipe_file 

445 input_image_path.as_posix(), # input path, 

446 (geometry.mask_file).as_posix(), # outputpath, 

447 ], 

448 capture_output=True, 

449 ) 

450 if output.returncode != 0: 450 ↛ 451line 450 didn't jump to line 451 because the condition on line 450 was never true

451 raise ValueError( 

452 f"Subprocess call for script {geometry.recipe_file} using executable 'python' did not execute correctly." 

453 ) 

454 # check for mask file 

455 if not geometry.mask_file.exists(): 455 ↛ 456line 455 didn't jump to line 456 because the condition on line 455 was never true

456 raise ValueError( 

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

458 ) 

459 # TODO 

460 # save masks 

461 

462 return True 

463 

464 

465# TODO add more complex patterning behavior 

466def mill_operation( 

467 step: tbt.Step, 

468 fib_settings: tbt.FIBSettings, 

469 general_settings: tbt.GeneralSettings, 

470 slice_number: int, 

471) -> bool: 

472 """ 

473 Perform a milling operation based on the provided step and settings. 

474 

475 This function performs a milling operation using the specified step, FIB settings, general settings, and slice number. 

476 

477 ## Parameters 

478 

479 - `step` (`tbt.Step`): The step object containing the operation settings. 

480 - `fib_settings` (`tbt.FIBSettings`): The FIB settings object containing the microscope and pattern settings. 

481 - `general_settings` (`tbt.GeneralSettings`): The general settings object. 

482 - `slice_number` (`int`): The slice number for the operation. 

483 

484 ## Returns 

485 

486 - `bool`: True if the milling operation is successful. 

487 

488 ## Raises 

489 

490 - `ValueError`: If the ion image for selected area milling is not found. 

491 """ 

492 microscope = fib_settings.microscope 

493 

494 shutter_control(microscope=microscope) 

495 # prepare beam 

496 img.imaging_device(microscope=microscope, beam=fib_settings.mill_beam) 

497 # set milling application and device 

498 

499 prepare_milling( 

500 microscope=microscope, 

501 application=fib_settings.pattern.application, 

502 ) 

503 

504 # get expected path of the fib image 

505 if fib_settings.pattern.type != tbt.FIBPatternType.SELECTED_AREA: 

506 input_image_path = None 

507 else: 

508 input_image_path = Path.join( 

509 general_settings.exp_dir, 

510 step.name, 

511 f"{slice_number:04}.tif", 

512 ) 

513 if not input_image_path.exists(): 

514 raise ValueError( 

515 f"Ion image for selected area milling was not found at '{input_image_path}'." 

516 ) 

517 

518 # make the pattern 

519 pattern = create_pattern( 

520 fib_settings.pattern.geometry, 

521 microscope=microscope, 

522 kwargs={"input_image_path": input_image_path}, 

523 ) 

524 

525 microscope.patterning.run() 

526 

527 return True