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
« 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.
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.
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.
16## Typical workflow
18from pytribeam.fib import mill_operation
20mill_operation(
21 step=step,
22 fib_settings=fib_settings,
23 general_settings=general_settings,
24 slice_number=0,
25)
27For workflows that need to prepare the microscope and create a pattern manually:
30from pytribeam.fib import prepare_milling, create_pattern
32prepare_milling(
33 microscope=microscope,
34 application="Si-ccs",
35)
37pattern = create_pattern(
38 geometry=fib_settings.pattern.geometry,
39 microscope=microscope,
40)
43## Main entry points
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.
56## Supported pattern geometries
58`create_pattern` is implemented as a `functools.singledispatch` function. The
59currently supported geometry types are:
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 |
68Unsupported geometry types raise `NotImplementedError`.
70## Units
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`.
77## Stream-pattern image processing
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`.
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.
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.
94<hr style="height: 12px; background-color: #333; border: none;">
95"""
97__all__ = [
98 "shutter_control",
99 "prepare_milling",
100 "create_pattern",
101 "image_processing",
102 "mill_operation",
103]
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
114# Autoscript included modules
115from PIL import Image as pil_img
116import cv2
117import numpy as np
119# 3rd party module
121# Local scripts
122import pytribeam.constants as cs
123from pytribeam.constants import Conversions
124import pytribeam.types as tbt
125import pytribeam.image as img
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.
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.
134 ## Parameters
136 - `microscope` (`tbt.Microscope`): The microscope object for which to control the e-beam shutter.
138 ## Raises
140 - `SystemError`: If the e-beam shutter is installed but cannot be set to automatic mode.
142 ## Warnings
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
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.
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.
176 ## Parameters
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).
182 ## Returns
184 - `bool`: True if the preparation is successful.
186 ## Raises
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)
204 return True
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.
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.
218 ## Parameters
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.
224 ## Returns
226 - `bool`: True if the pattern is created successfully.
228 ## Raises
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)}")
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.
247 ## Parameters
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.
253 ## Returns
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
267 return pattern
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.
279 ## Parameters
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.
285 ## Returns
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
299 return pattern
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.
311 ## Parameters
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.
317 ## Returns
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
331 return pattern
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.
343 ## Parameters
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.
349 ## Returns
351 - `tbt.StreamPattern`: The created stream pattern.
352 """
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 )
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)
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
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
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 ]
408 stream_def.repeat_count = geometry.repeats
410 stream_pattern = microscope.patterning.create_stream(
411 center_x=0.0,
412 center_y=0.0,
413 stream_pattern_definition=stream_def,
414 )
416 return stream_pattern
419def image_processing(
420 geometry: tbt.FIBStreamPattern,
421 input_image_path: Path,
422) -> bool:
423 """
424 Perform image processing for FIB stream pattern.
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.
428 ## Parameters
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.
433 ## Returns
435 - `bool`: True if the image processing is successful.
437 ## Raises
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
462 return True
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.
475 This function performs a milling operation using the specified step, FIB settings, general settings, and slice number.
477 ## Parameters
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.
484 ## Returns
486 - `bool`: True if the milling operation is successful.
488 ## Raises
490 - `ValueError`: If the ion image for selected area milling is not found.
491 """
492 microscope = fib_settings.microscope
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
499 prepare_milling(
500 microscope=microscope,
501 application=fib_settings.pattern.application,
502 )
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 )
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 )
525 microscope.patterning.run()
527 return True