Coverage for src/pytribeam/factory.py: 43%
682 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-10-23 20:42 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-10-23 20:42 +0000
1#!/usr/bin/python3
2"""Factory and validation functions for `pytribeam` settings objects.
4This module converts raw configuration dictionaries and live microscope state
5into structured `pytribeam.types` objects. It is the main construction layer used
6by the workflow to validate YAML input, read current hardware settings, enforce
7limits, and create typed settings for imaging, FIB, laser, EBSD, EDS, custom,
8stage, beam, detector, and scan operations.
10Most users should not need to call the low-level validation helpers directly.
11Workflow code typically enters this module through `general`, `step`, or one of
12the `active_*` functions.
14## Typical usage
16Create experiment-wide general settings from a parsed YAML dictionary:
18```python
19import pytribeam.factory as factory
21general_settings = factory.general(
22 general_db=general_db,
23 yml_format=yml_format,
24)
25```
27Create a typed workflow step from a YAML step dictionary:
29```python
30step = factory.step(
31 microscope=microscope,
32 step_name=step_name,
33 step_settings=step_settings,
34 general_settings=general_settings,
35 yml_format=yml_format,
36)
37```
39Read the current microscope state into `pytribeam` settings objects:
41```python
42beam = factory.active_beam_with_settings(microscope)
43image_settings = factory.active_image_settings(microscope)
44stage_position = factory.active_stage_position_settings(microscope)
45```
47## Main entry points
49- `general`: validate and create `tbt.GeneralSettings`.
50- `step`: validate and create a `tbt.Step` for the configured step type.
51- `active_image_settings`: read the current microscope imaging state.
52- `active_beam_with_settings`: read the active beam and beam settings.
53- `active_detector_settings`: read the active detector settings.
54- `active_scan_settings`: read the active scan settings.
55- `active_stage_position_settings`: read the current stage position in user
56 units.
57- `active_laser_state`: read the current laser state.
58- `stage_limits`, `beam_limits`, and `scan_limits`: read hardware limits from
59 the active microscope connection.
61## Configuration workflow
63The workflow module uses this factory layer to transform parsed YAML data into
64validated runtime objects:
66```text
67YAML file
68 -> raw dictionaries
69 -> validation helpers
70 -> typed settings objects
71 -> workflow execution
72```
74For example, image, FIB, laser, EBSD, EDS, and custom steps are all constructed
75from dictionaries using step-type-specific factory functions. These functions
76validate required keys, convert strings to enums, check numeric limits, and
77return the appropriate `tbt.*Settings` object.
79## Validation behavior
81Validation helpers check user configuration against package schema information,
82supported enum values, microscope hardware limits, and workflow constraints.
83Invalid settings generally raise `ValueError`, `TypeError`, `KeyError`, or
84`NotImplementedError` with context about the step or setting that failed.
86## Active-state helpers
88Functions beginning with `active_` read the current microscope or laser state and
89return a corresponding `pytribeam.types` object. These are useful for logging,
90GUI display, default-setting generation, and validating that requested hardware
91changes were applied successfully.
93## Units
95Factory functions preserve the package-wide unit conventions used by
96`pytribeam.types`. User-facing values generally use explicit suffixes such as
97`_mm`, `_um`, `_deg`, `_kv`, `_na`, and `_us`, while values read from or written
98to hardware APIs may be converted internally as needed.
100> **Warning**
101>
102> Many functions in this module query live microscope or laser state. Ensure that
103> the required hardware connections and external APIs are available before using
104> active-state or hardware-limit factory functions.
106<hr style="height: 12px; background-color: #333; border: none;">
107"""
109__all__ = [
110 "active_fib_applications",
111 "active_beam_with_settings",
112 "active_detector_settings",
113 "active_image_settings",
114 "active_imaging_device",
115 "active_scan_settings",
116 "active_stage_position_settings",
117 "active_laser_state",
118 "active_laser_settings",
119 "available_detector_types",
120 "available_detector_modes",
121 "beam_object_type",
122 "stage_limits",
123 "beam_limits",
124 "scan_limits",
125 "general",
126 "image",
127 "fib",
128 "laser",
129 "ebsd",
130 "eds",
131 "custom",
132 "step",
133 "string_to_res",
134 "valid_string_resolution",
135]
137## python standard libraries
138from pathlib import Path
139from typing import List, Union
140import warnings
141from functools import singledispatch
142import math
145# 3rd party libraries
146from schema import And, Or, Schema
148# Local
149import pytribeam.insertable_devices as devices
150import pytribeam.image as img
151import pytribeam.utilities as ut
152import pytribeam.stage as stage
153from pytribeam.constants import Conversions, Constants
154from pytribeam.utilities import application_files
156try:
157 import pytribeam.laser as fs_laser
158except:
159 pass
160import pytribeam.types as tbt
163def active_fib_applications(
164 microscope: tbt.Microscope,
165) -> list:
166 """
167 Retrieve a list of all active FIB (Focused Ion Beam) patterning application files from the microscope.
169 ## Parameters
171 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the application files.
173 ## Returns
175 - `list`: A list of active FIB patterning application files.
177 """
178 return application_files(microscope)
181def active_beam_with_settings(
182 microscope: tbt.Microscope,
183) -> tbt.Beam:
184 """
185 Retrieve the current active beam and its settings from the microscope to create a beam object.
187 This function grabs the current beam and its settings on the microscope to make a beam object. These settings fully depend on the currently active beam as determined by xTUI. Tolerance values for voltage and current are auto-populated as a ratio of current values predetermined in the `Constants` class.
189 ## Parameters
191 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active beam and its settings.
193 ## Returns
195 - `tbt.Beam`: The active beam object with its settings.
196 """
197 selected_beam = active_imaging_device(microscope=microscope)
198 beam = ut.beam_type(selected_beam, microscope)
200 voltage_kv = round(beam.high_voltage.value * Conversions.V_TO_KV, 6)
201 voltage_tol_kv = round(voltage_kv * Constants.voltage_tol_ratio, 6)
203 current_na = round(beam.beam_current.value * Conversions.A_TO_NA, 6)
204 current_tol_na = round(current_na * Constants.current_tol_ratio, 6)
206 hfw_mm = round(beam.horizontal_field_width.value * Conversions.M_TO_MM, 6)
208 working_dist_mm = round(beam.working_distance.value * Conversions.M_TO_MM, 6)
210 angular_correction = getattr(beam, "angular_correction", None)
211 if angular_correction is None: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 dynamic_focus = None
213 tilt_correction = None
214 else:
215 dynamic_focus = bool(angular_correction.dynamic_focus.is_on)
216 tilt_correction = bool(angular_correction.tilt_correction.is_on)
218 active_settings = tbt.BeamSettings(
219 voltage_kv=voltage_kv,
220 current_na=current_na,
221 hfw_mm=hfw_mm,
222 working_dist_mm=working_dist_mm,
223 voltage_tol_kv=voltage_tol_kv,
224 current_tol_na=current_tol_na,
225 dynamic_focus=dynamic_focus,
226 tilt_correction=tilt_correction,
227 )
229 return type(selected_beam)(settings=active_settings)
232def active_detector_settings(
233 microscope: tbt.Microscope,
234) -> tbt.Detector:
235 """
236 Retrieve the current active detector settings from the microscope to create a detector object.
238 This function grabs the current detector settings on the microscope to make a detector object. These settings fully depend on the currently active detector as determined by xTUI.
240 If the microscope reports a detector mode that the detector does not offer (for example "None" for a single-mode detector), the detector's first available mode is used instead and a warning is issued, so that the settings pass `validate_detector_settings`.
242 ## Parameters
244 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active detector settings.
246 ## Returns
248 - `tbt.Detector`: The active detector object with its settings.
249 """
251 detector_type = microscope.detector.type.value
252 detector_mode = microscope.detector.mode.value
253 # The microscope can report a mode the detector does not offer (e.g. "None"
254 # for a single-mode detector). Report one it does, so these settings pass
255 # validate_detector_settings.
256 available_modes = list(microscope.detector.mode.available_values)
257 if available_modes and detector_mode not in available_modes: 257 ↛ 258, 257 ↛ 2642 missed branches: 1) line 257 didn't jump to line 258 because the condition on line 257 was never true, 2) line 257 didn't jump to line 264 because the condition on line 257 was always true
258 warnings.warn(
259 f'Detector mode "{detector_mode}" is not available on the '
260 f'"{detector_type}" detector; using "{available_modes[0]}". '
261 f"Available modes: {available_modes}."
262 )
263 detector_mode = available_modes[0]
264 brightness = microscope.detector.brightness.value
265 contrast = microscope.detector.contrast.value
266 auto_cb_settings = tbt.ScanArea(
267 left=None,
268 top=None,
269 width=None,
270 height=None,
271 )
272 custom_settings = None
274 active_detector = tbt.Detector(
275 type=tbt.DetectorType(detector_type),
276 mode=tbt.DetectorMode(detector_mode),
277 brightness=brightness,
278 contrast=contrast,
279 auto_cb_settings=auto_cb_settings,
280 custom_settings=custom_settings,
281 )
283 return active_detector
286def active_image_settings(microscope: tbt.Microscope) -> tbt.ImageSettings:
287 """
288 Retrieve the current active image settings from the microscope to create an image settings object.
290 This function grabs the current beam, detector, and scan settings on the microscope to make an image settings object. The bit depth is set to the default color depth defined in the `Constants` class.
292 ## Parameters
294 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active image settings.
296 ## Returns
298 - `tbt.ImageSettings`: The active image settings object.
299 """
300 beam = active_beam_with_settings(microscope=microscope)
301 detector = active_detector_settings(microscope=microscope)
302 scan = active_scan_settings(microscope=microscope)
303 bit_depth = Constants.default_color_depth
305 active_image_settings = tbt.ImageSettings(
306 microscope=microscope,
307 beam=beam,
308 detector=detector,
309 scan=scan,
310 bit_depth=bit_depth,
311 )
313 return active_image_settings
316def active_imaging_device(microscope: tbt.Microscope) -> tbt.Beam:
317 """
318 Determine the active imaging device and return the corresponding internal beam type object with null beam settings.
320 This function identifies the currently active imaging device on the microscope and returns the appropriate beam type object (electron or ion) with null beam settings.
322 ## Parameters
324 - `microscope` (`tbt.Microscope`): The microscope object from which to determine the active imaging device.
326 ## Returns
328 - `tbt.Beam`: The active beam object with null beam settings.
330 ## Raises
332 - `ValueError`: If the currently selected device is neither an electron beam nor an ion beam.
333 """
334 curr_device = tbt.Device(microscope.imaging.get_active_device())
335 if curr_device == tbt.Device.ELECTRON_BEAM:
336 selected_beam = beam_object_type(type=tbt.BeamType.ELECTRON)(
337 settings=tbt.BeamSettings()
338 )
339 elif curr_device == tbt.Device.ION_BEAM: 339 ↛ 340, 339 ↛ 3442 missed branches: 1) line 339 didn't jump to line 340 because the condition on line 339 was never true, 2) line 339 didn't jump to line 344 because the condition on line 339 was always true
340 selected_beam = beam_object_type(type=tbt.BeamType.ION)(
341 settings=tbt.BeamSettings()
342 )
343 else:
344 raise ValueError(
345 f"Currently selected device {curr_device}, make sure a quadrant in xTUI with either an Electron beam or Ion beam active is selected."
346 )
347 return selected_beam
350def active_scan_settings(
351 microscope: tbt.Microscope,
352) -> tbt.Scan:
353 """
354 Retrieve the current active scan settings from the microscope to create a scan object.
356 This function grabs the current scan settings on the microscope to make a scan object. These settings fully depend on the currently active scan settings as determined by xTUI.
358 ## Parameters
360 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active scan settings.
362 ## Returns
364 - `tbt.Scan`: The active scan object with its settings.
365 """
366 selected_beam = active_imaging_device(microscope=microscope)
367 beam = ut.beam_type(selected_beam, microscope)
369 rotation_deg = beam.scanning.rotation.value * Conversions.RAD_TO_DEG
370 dwell_time_us = beam.scanning.dwell_time.value * Conversions.S_TO_US
371 scan_res = beam.scanning.resolution.value
372 scan_res = string_to_res(scan_res)
373 if ut.valid_enum_entry(obj=scan_res, check_type=tbt.PresetResolution):
374 scan_res = tbt.PresetResolution(scan_res)
376 active_scan = tbt.Scan(
377 rotation_deg=rotation_deg,
378 dwell_time_us=dwell_time_us,
379 resolution=scan_res,
380 )
382 return active_scan
385def active_stage_position_settings(microscope: tbt.Microscope) -> tbt.StagePositionUser:
386 """
387 Retrieve the current stage position in the raw coordinate system and user units [mm, deg].
389 This function sets the stage coordinate system to RAW, retrieves the current stage position in encoder units (meters and radians), converts it to user units (millimeters and degrees), and ensures the r-axis is within the axis limit.
391 ## Parameters
393 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the current stage position.
395 ## Returns
397 - `tbt.StagePositionUser`: The current stage position in user units [mm, deg].
398 """
399 stage.coordinate_system(microscope=microscope, mode=tbt.StageCoordinateSystem.RAW)
400 # encoder positions (pos) are in meters and radians
401 direct_encoder_pos = microscope.specimen.stage.current_position
402 x_m, y_m, z_m = direct_encoder_pos.x, direct_encoder_pos.y, direct_encoder_pos.z
403 r_rad, t_rad = direct_encoder_pos.r, direct_encoder_pos.t
404 coord_system_str = direct_encoder_pos.coordinate_system
406 encoder_pos = tbt.StagePositionEncoder(
407 x=x_m, y=y_m, z=z_m, r=r_rad, t=t_rad, coordinate_system=coord_system_str
408 )
409 user_pos = stage.encoder_to_user_position(encoder_pos)
411 # ensure r-axis is kept in axis limit
412 # ------------------------------------------------
413 # THIS IS NOT NEEDED, AUTOSCRIPT DOES THIS ALREADY
414 # ------------------------------------------------
415 # if not ut.in_interval(
416 # val=user_pos.r_deg,
417 # limit=Constants.rotation_axis_limit_deg,
418 # type=tbt.IntervalType.RIGHT_OPEN,
419 # ):
420 # # used as right-open internal: 180.0 is not valid and should be converted to -180.0
421 # while user_pos.r_deg >= Constants.rotation_axis_limit_deg.max:
422 # new_r_deg = user_pos.r_deg - 360.0
423 # while user_pos.r_deg < Constants.rotation_axis_limit_deg.min:
424 # new_r_deg = user_pos.r_deg + 360.0
425 # new_r_deg = round(new_r_deg, 6)
426 # user_pos = tbt.StagePositionUser(
427 # x_mm=user_pos.x_mm,
428 # y_mm=user_pos.y_mm,
429 # z_mm=user_pos.z_mm,
430 # r_deg=new_r_deg,
431 # t_deg=user_pos.t_deg,
432 # )
434 return user_pos
437def active_laser_state() -> tbt.LaserState:
438 """
439 Retrieve the current state of the laser, including various properties that can be quickly read.
441 This function returns a dictionary object for all properties that can be quickly read from the laser (not exhaustive). Power can be read but has its own method and is more involved. Flipper configuration can only be set, not read.
443 ## Returns
445 tbt.LaserState
446 The current state of the laser, including wavelength, frequency, pulse divider, pulse energy, objective position, beam shift, pattern, and expected pattern duration.
448 ## Raises
450 KeyError
451 If an unsupported LaserPatternType is encountered.
453 """
454 vals = fs_laser.tfs_laser.Laser_ReadValues()
455 vals["objective_position_mm"] = fs_laser.tfs_laser.LIP_GetZPosition()
456 vals["beam_shift_um_x"] = fs_laser.tfs_laser.BeamShift_Get_X()
457 vals["beam_shift_um_y"] = fs_laser.tfs_laser.BeamShift_Get_Y()
458 vals["shutter_state"] = fs_laser.tfs_laser.Shutter_GetState()
459 vals["pattern"] = fs_laser.tfs_laser.Patterning_ReadValues()
460 vals["expected_pattern_duration_s"] = (
461 fs_laser.tfs_laser.Patterning_GetExpectedDuration()
462 )
464 pattern_db = vals["pattern"]
465 pattern_type = pattern_db["patternType"].lower()
466 if not ut.valid_enum_entry(pattern_type, tbt.LaserPatternType):
467 raise KeyError( 467 ↛ exit, 467 ↛ exit2 missed branches: 1) line 467 didn't jump to the function exit, 2) line 467 didn't except from function 'active_laser_state' because the raise on line 467 wasn't executed
468 f"Unsupported LaserPatternType of {pattern_type}, supported types include: {[i.value for i in tbt.LaserPatternType]}"
469 )
470 pattern_type = tbt.LaserPatternType(pattern_type)
471 mode = tbt.LaserPatternMode(pattern_db["patterningMode"].lower())
472 if mode == tbt.LaserPatternMode.COARSE: 472 ↛ 475line 472 didn't jump to line 475 because the condition on line 472 was always true
473 pixel_dwell_ms = pattern_db["dwellTime"]
474 pulses_per_pixel = None
475 elif mode == tbt.LaserPatternMode.FINE: 475 ↛ 478line 475 didn't jump to line 478 because the condition on line 475 was always true
476 pixel_dwell_ms = None
477 pulses_per_pixel = pattern_db["pulsesPerPixel"]
478 rotation_deg = pattern_db["patternRotation_deg"]
480 if pattern_type == tbt.LaserPatternType.BOX: 480 ↛ 492line 480 didn't jump to line 492 because the condition on line 480 was always true
481 geometry = tbt.LaserBoxPattern(
482 passes=pattern_db["passes"],
483 size_x_um=pattern_db["xSize_um"],
484 size_y_um=pattern_db["ySize_um"],
485 pitch_x_um=pattern_db["xPitch_um"],
486 pitch_y_um=pattern_db["yPitch_um"],
487 scan_type=tbt.LaserScanType(pattern_db["scanningMode"].lower()),
488 coordinate_ref=tbt.CoordinateReference(
489 pattern_db["coordReference"].lower()
490 ),
491 )
492 if pattern_type == tbt.LaserPatternType.LINE: 492 ↛ 493, 492 ↛ 5002 missed branches: 1) line 492 didn't jump to line 493 because the condition on line 492 was never true, 2) line 492 didn't jump to line 500 because the condition on line 492 was always true
493 geometry = tbt.LaserLinePattern(
494 passes=pattern_db["passes"],
495 size_um=pattern_db["xSize_um"],
496 pitch_um=pattern_db["xPitch_um"],
497 scan_type=tbt.LaserScanType(pattern_db["scanningMode"].lower()),
498 )
500 pattern = tbt.LaserPattern(
501 mode=mode,
502 rotation_deg=rotation_deg,
503 geometry=geometry,
504 pulses_per_pixel=pulses_per_pixel,
505 pixel_dwell_ms=pixel_dwell_ms,
506 )
508 state = tbt.LaserState(
509 wavelength_nm=vals["wavelength_nm"],
510 frequency_khz=vals["frequency_kHz"],
511 pulse_divider=vals["pulse_divider"],
512 pulse_energy_uj=vals["pulse_energy_uJ"],
513 objective_position_mm=vals["objective_position_mm"],
514 beam_shift_um=tbt.Point(x=vals["beam_shift_um_x"], y=vals["beam_shift_um_y"]),
515 pattern=pattern,
516 expected_pattern_duration_s=vals["expected_pattern_duration_s"],
517 )
519 return state
522def active_laser_settings(microscope: tbt.Microscope) -> tbt.LaserSettings:
523 """
524 Retrieve the current active laser settings from the microscope to create a laser settings object.
526 This function grabs the current laser state and uses it to create a laser settings object. Some values cannot be read by Laser Control and can only be set. For example, polarization will default to "Vertical" as this value cannot be read.
528 ## Parameters
530 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active laser settings.
532 ## Returns
534 - `tbt.LaserSettings`: The active laser settings object.
535 """
536 state = active_laser_state()
538 settings = tbt.LaserSettings(
539 microscope=microscope,
540 pulse=tbt.LaserPulse(
541 wavelength_nm=state.wavelength_nm,
542 divider=state.pulse_divider,
543 energy_uj=state.pulse_energy_uj,
544 polarization=tbt.LaserPolarization.VERTICAL, # TODO, can't read this, can only set it
545 ),
546 objective_position_mm=state.objective_position_mm,
547 beam_shift_um=state.beam_shift_um,
548 pattern=state.pattern,
549 )
551 return settings
554def available_detector_types(microscope: tbt.Microscope) -> List[str]:
555 """
556 Retrieve the available detector types on the current microscope.
558 This function returns a list of available detector types on the current microscope.
560 ## Parameters
562 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the available detector types.
564 ## Returns
566 - `List[str]`: A list of available detector types.
567 """
568 detectors = microscope.detector.type.available_values
569 return detectors
572def available_detector_modes(microscope: tbt.Microscope) -> List[str]:
573 """
574 Retrieve the available detector modes on the current microscope.
576 This function returns a list of available detector modes on the current microscope.
578 ## Parameters
580 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the available detector modes.
582 ## Returns
584 - `List[str]`: A list of available detector modes.
585 """
586 modes = microscope.detector.mode.available_values
587 # available = [tbt.DetectorType(i) for i in modes]
588 return modes
591def beam_object_type(type: tbt.BeamType) -> tbt.Beam:
592 """
593 Retrieve the beam object type based on the given beam type.
595 This function returns the appropriate beam object type (electron or ion) based on the provided beam type.
597 ## Parameters
599 - `type` (`tbt.BeamType`): The type of the beam (electron or ion).
601 ## Returns
603 - `tbt.Beam`: The corresponding beam object type.
605 ## Raises
607 - `NotImplementedError`: If the provided beam type is unsupported.
608 """
609 if not isinstance(type, tbt.BeamType):
610 raise ValueError("Electron and Ion are the only allowed beam object types.")
611 if type.value == "electron":
612 return tbt.ElectronBeam
613 if type.value == "ion":
614 return tbt.IonBeam
617def stage_limits(microscope: tbt.Microscope) -> tbt.StageLimits:
618 """
619 Retrieve the stage limits from the current microscope connection.
621 This function retrieves the stage limits for the X, Y, Z, R, and T axes from the current microscope connection and returns them as a `StageLimits` object.
623 ## Parameters
625 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the stage limits.
627 ## Returns
629 - `tbt.StageLimits`: The stage limits for the X, Y, Z, R, and T axes in user units (mm and degrees).
630 """
631 stage.coordinate_system(microscope=microscope)
632 # x position
633 min_x_mm = (
634 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.X).min
635 * Conversions.M_TO_MM
636 )
637 max_x_mm = (
638 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.X).max
639 * Conversions.M_TO_MM
640 )
641 # y position
642 min_y_mm = (
643 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.Y).min
644 * Conversions.M_TO_MM
645 )
646 max_y_mm = (
647 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.Y).max
648 * Conversions.M_TO_MM
649 )
650 # z position
651 min_z_mm = (
652 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.Z).min
653 * Conversions.M_TO_MM
654 )
655 max_z_mm = (
656 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.Z).max
657 * Conversions.M_TO_MM
658 )
659 # r position
660 min_r_deg = Constants.rotation_axis_limit_deg.min
661 max_r_deg = Constants.rotation_axis_limit_deg.max
662 # t position
663 min_t_deg = (
664 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.T).min
665 * Conversions.RAD_TO_DEG
666 )
667 max_t_deg = (
668 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.T).max
669 * Conversions.RAD_TO_DEG
670 )
672 return tbt.StageLimits(
673 x_mm=tbt.Limit(min=min_x_mm, max=max_x_mm),
674 y_mm=tbt.Limit(min=min_y_mm, max=max_y_mm),
675 z_mm=tbt.Limit(min=min_z_mm, max=max_z_mm),
676 r_deg=tbt.Limit(min=min_r_deg, max=max_r_deg),
677 t_deg=tbt.Limit(min=min_t_deg, max=max_t_deg),
678 )
681def beam_limits(
682 selected_beam: property,
683 beam_type: tbt.BeamType,
684) -> tbt.BeamLimits:
685 """
686 Retrieve the beam limits for the selected beam and beam type.
688 This function retrieves the limits for voltage, current, horizontal field width (HFW), and working distance for the selected beam and beam type, and returns them as a `BeamLimits` object.
690 ## Parameters
692 - `selected_beam` (`property`): The selected beam property from which to retrieve the limits.
693 - `beam_type` (`tbt.BeamType`): The type of the beam (electron or ion).
695 ## Returns
697 - `tbt.BeamLimits`: The beam limits for voltage, current, HFW, and working distance in user units (kV, nA, mm).
699 ## Raises
701 - `ValueError`: If the beam type is unsupported.
702 """
703 # voltage range
704 min_kv = selected_beam.high_voltage.limits.min * Conversions.V_TO_KV
705 max_kv = selected_beam.high_voltage.limits.max * Conversions.V_TO_KV
707 # current range
708 if beam_type == tbt.BeamType.ELECTRON: 708 ↛ 711line 708 didn't jump to line 711 because the condition on line 708 was always true
709 min_na = selected_beam.beam_current.limits.min * Conversions.A_TO_NA
710 max_na = selected_beam.beam_current.limits.max * Conversions.A_TO_NA
711 if beam_type == tbt.BeamType.ION: 711 ↛ 717line 711 didn't jump to line 717 because the condition on line 711 was always true
712 available_currents = selected_beam.beam_current.available_values
713 min_na = min(available_currents) * Conversions.A_TO_NA
714 max_na = max(available_currents) * Conversions.A_TO_NA
716 # hfw range
717 min_hfw_mm = selected_beam.horizontal_field_width.limits.min * Conversions.M_TO_MM
718 max_hfw_mm = selected_beam.horizontal_field_width.limits.max * Conversions.M_TO_MM
720 # working_dist range
721 min_wd_mm = selected_beam.working_distance.limits.min * Conversions.M_TO_MM
722 max_wd_mm = selected_beam.working_distance.limits.max * Conversions.M_TO_MM
724 return tbt.BeamLimits(
725 voltage_kv=tbt.Limit(min=min_kv, max=max_kv),
726 current_na=tbt.Limit(min=min_na, max=max_na),
727 hfw_mm=tbt.Limit(min=min_hfw_mm, max=max_hfw_mm),
728 working_distance_mm=tbt.Limit(min=min_wd_mm, max=max_wd_mm),
729 )
732def general(
733 general_db: dict,
734 yml_format: tbt.YMLFormatVersion,
735) -> tbt.GeneralSettings:
736 """
737 Convert a general settings dictionary to a built-in type and perform schema checking.
739 This function converts a general settings dictionary from a .yml file to a `GeneralSettings` object. It performs schema checking to ensure valid inputs are requested.
741 ## Parameters
743 - `general_db` (`dict`): The general settings dictionary from the .yml file.
744 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
746 ## Returns
748 - `tbt.GeneralSettings`: The general settings object.
750 ## Raises
752 - `NotImplementedError`: If the provided yml format is unsupported.
753 """
755 if not yml_format in tbt.YMLFormatVersion:
756 raise NotImplementedError(
757 """Due to the complexity and number of variables,
758 image objects should only be constructed using a yml file."""
759 )
760 if not isinstance(yml_format, tbt.YMLFormatVersion):
761 raise NotImplementedError(f"Unsupported yml format of {yml_format}.")
763 validate_general_settings(
764 settings=general_db,
765 yml_format=yml_format,
766 )
768 if yml_format.version >= 1.0:
769 # slice thickness
770 slice_thickness_um = general_db["slice_thickness_um"]
771 # max slice number
772 max_slice_number = general_db["max_slice_num"]
773 # pre tilt
774 pre_tilt_deg = general_db["pre_tilt_deg"]
775 # sectioning axis
776 sectioning_axis = tbt.SectioningAxis(general_db["sectioning_axis"])
777 # stage tolerance
778 stage_tolerance = tbt.StageTolerance(
779 translational_um=general_db["stage_translational_tol_um"],
780 angular_deg=general_db["stage_angular_tol_deg"],
781 )
782 # connection
783 connection = tbt.MicroscopeConnection(
784 general_db["connection_host"], general_db["connection_port"]
785 )
786 # EBSD OEM
787 ebsd_oem = tbt.ExternalDeviceOEM(general_db["EBSD_OEM"])
788 # EDS OEM
789 eds_oem = tbt.ExternalDeviceOEM(general_db["EDS_OEM"])
790 # exp dir
791 exp_dir = general_db["exp_dir"]
792 # h5 log name
793 h5_log_name = general_db["h5_log_name"]
794 # remove log file extension if the user provided it
795 log_extension = Constants.logfile_extension
796 if h5_log_name.endswith(log_extension): 796 ↛ 799line 796 didn't jump to line 799 because the condition on line 796 was always true
797 h5_log_name = h5_log_name[: -len(log_extension)]
798 # step count
799 step_count = general_db["step_count"]
800 yml_version = 1.0
802 general_settings = tbt.GeneralSettings(
803 yml_version=yml_version,
804 slice_thickness_um=slice_thickness_um,
805 max_slice_number=max_slice_number,
806 pre_tilt_deg=pre_tilt_deg,
807 sectioning_axis=sectioning_axis,
808 stage_tolerance=stage_tolerance,
809 connection=connection,
810 EBSD_OEM=ebsd_oem,
811 EDS_OEM=eds_oem,
812 exp_dir=Path(exp_dir),
813 h5_log_name=h5_log_name,
814 step_count=step_count,
815 )
817 return general_settings
820def laser_box_pattern(settings: dict) -> tbt.LaserBoxPattern:
821 """
822 Convert a dictionary of laser box pattern settings to a `LaserBoxPattern` object.
824 This function takes a dictionary of laser box pattern settings and converts it to a `LaserBoxPattern` object.
826 ## Parameters
828 - `settings` (`dict`): The dictionary containing laser box pattern settings.
830 ## Returns
832 - `tbt.LaserBoxPattern`: The laser box pattern object.
833 """
834 return tbt.LaserBoxPattern(
835 passes=settings["passes"],
836 size_x_um=settings["size_x_um"],
837 size_y_um=settings["size_y_um"],
838 pitch_x_um=settings["pitch_x_um"],
839 pitch_y_um=settings["pitch_y_um"],
840 scan_type=tbt.LaserScanType(settings["scan_type"]),
841 coordinate_ref=tbt.CoordinateReference(settings["coordinate_ref"]),
842 )
845def laser_line_pattern(settings: dict) -> tbt.LaserBoxPattern:
846 """
847 Convert a dictionary of laser line pattern settings to a `LaserLinePattern` object.
849 This function takes a dictionary of laser line pattern settings and converts it to a `LaserLinePattern` object.
851 ## Parameters
853 - `settings` (`dict`): The dictionary containing laser line pattern settings.
855 ## Returns
857 - `tbt.LaserLinePattern`: The laser line pattern object.
858 """
859 return tbt.LaserLinePattern(
860 passes=settings["passes"],
861 size_um=settings["size_um"],
862 pitch_um=settings["pitch_um"],
863 scan_type=tbt.LaserScanType(settings["scan_type"]),
864 )
867def laser(
868 microscope: tbt.Microscope,
869 step_settings: dict,
870 step_name: str,
871 yml_format: tbt.YMLFormatVersion,
872) -> tbt.LaserSettings:
873 """
874 Convert a laser step from a .yml file to microscope settings for performing laser milling.
876 This function converts a laser step from a .yml file to `LaserSettings` for the microscope. It performs schema checking to ensure valid inputs are requested.
878 ## Parameters
880 - `microscope` (`tbt.Microscope`): The microscope object for which to set the laser settings.
881 - `step_settings` (`dict`): The dictionary containing the laser step settings from the .yml file.
882 - `step_name` (`str`): The name of the step in the .yml file.
883 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
885 ## Returns
887 - `tbt.LaserSettings`: The laser settings object.
889 ## Raises
891 - `KeyError`: If required settings are missing from the .yml file.
892 """
893 if yml_format.version >= 1.0:
894 # pulse settings
895 pulse_set_db = step_settings.get("pulse")
896 if pulse_set_db is None:
897 raise KeyError(
898 f"Invalid .yml file, no 'pulse' settings found in 'laser' step_type for step '{step_name}'."
899 )
900 # laser optics settings
901 optics_set_db = {
902 "objective_position_mm": step_settings.get("objective_position_mm"),
903 "beam_shift_um_x": step_settings.get("beam_shift").get("x_um"),
904 "beam_shift_um_y": step_settings.get("beam_shift").get("y_um"),
905 }
906 # pattern settings
907 pattern_set_db = step_settings.get("pattern")
908 if pattern_set_db is None: 908 ↛ 912line 908 didn't jump to line 912 because the condition on line 908 was always true
909 raise KeyError(
910 f"Invalid .yml file, no 'pattern' settings found in 'laser' step_type for step '{step_name}'."
911 )
912 line_pattern_db = pattern_set_db.get("type").get("line")
913 box_pattern_db = pattern_set_db.get("type").get("box")
915 validate_pulse_settings(
916 settings=pulse_set_db,
917 yml_format=yml_format,
918 step_name=step_name,
919 )
920 pulse = tbt.LaserPulse(
921 wavelength_nm=tbt.LaserWavelength(pulse_set_db["wavelength_nm"]),
922 divider=pulse_set_db["divider"],
923 energy_uj=pulse_set_db["energy_uj"],
924 polarization=tbt.LaserPolarization(pulse_set_db["polarization"]),
925 )
927 validate_laser_optics_settings(
928 settings=optics_set_db,
929 yml_format=yml_format,
930 step_name=step_name,
931 )
933 pattern_type = validate_laser_pattern_settings(
934 settings=pattern_set_db,
935 yml_format=yml_format,
936 step_name=step_name,
937 )
938 if pattern_type == tbt.LaserPatternType.BOX: 938 ↛ 940line 938 didn't jump to line 940 because the condition on line 938 was always true
939 geometry = laser_box_pattern(box_pattern_db)
940 if pattern_type == tbt.LaserPatternType.LINE: 940 ↛ 942line 940 didn't jump to line 942 because the condition on line 940 was always true
941 geometry = laser_line_pattern(line_pattern_db)
942 pattern = tbt.LaserPattern(
943 mode=tbt.LaserPatternMode(pattern_set_db["mode"]),
944 rotation_deg=pattern_set_db["rotation_deg"],
945 pulses_per_pixel=pattern_set_db["pulses_per_pixel"],
946 pixel_dwell_ms=pattern_set_db["pixel_dwell_ms"],
947 geometry=geometry,
948 )
950 laser_settings = tbt.LaserSettings(
951 microscope=microscope,
952 pulse=pulse,
953 objective_position_mm=optics_set_db["objective_position_mm"],
954 beam_shift_um=tbt.Point(
955 x=optics_set_db["beam_shift_um_x"],
956 y=optics_set_db["beam_shift_um_y"],
957 ),
958 pattern=pattern,
959 )
961 return laser_settings
964def image(
965 microscope: tbt.Microscope,
966 step_settings: dict,
967 step_name: str,
968 yml_format: tbt.YMLFormatVersion,
969) -> tbt.ImageSettings:
970 """
971 Convert an image step from a .yml file to microscope settings for capturing an image.
973 This function converts an image step from a .yml file to `ImageSettings` for the microscope. It performs schema checking to ensure valid inputs are requested.
975 ## Parameters
977 - `microscope` (`tbt.Microscope`): The microscope object for which to set the image settings.
978 - `step_settings` (`dict`): The dictionary containing the image step settings from the .yml file.
979 - `step_name` (`str`): The name of the step in the .yml file.
980 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
982 ## Returns
984 - `tbt.ImageSettings`: The image settings object.
986 ## Raises
988 - `KeyError`: If required settings are missing from the .yml file.
989 - `NotImplementedError`: If the provided beam type is unsupported.
990 - `ValueError`: If invalid scan rotation is requested with dynamic focus or tilt correction, or if the bit depth is unsupported.
991 """
992 if yml_format.version >= 1.0: 992 ↛ 993, 992 ↛ 10362 missed branches: 1) line 992 didn't jump to line 993 because the condition on line 992 was never true, 2) line 992 didn't jump to line 1036 because the condition on line 992 was always true
993 step_general = step_settings.get(yml_format.step_general_key)
994 if step_general is None:
995 raise KeyError(
996 f"Invalid .yml file, no 'step_general' settings found in step '{step_name}'."
997 )
998 step_type = step_general.get(yml_format.step_type_key)
999 if step_type is None:
1000 raise KeyError(
1001 f"Invalid .yml file, no 'step_type' settings found in step '{step_name}'."
1002 )
1003 # beam settings
1004 beam_set_db = step_settings.get("beam")
1005 if beam_set_db is None: 1005 ↛ 1006line 1005 didn't jump to line 1006 because the condition on line 1005 was never true
1006 raise KeyError(
1007 f"Invalid .yml file, no 'beam' settings found in '{step_type}' step_type for step '{step_name}'."
1008 )
1009 beam_type_value = beam_set_db.get("type")
1010 if not ut.valid_enum_entry(beam_type_value, tbt.BeamType):
1011 raise NotImplementedError(
1012 f"Unsupported beam type of '{beam_type_value}', supported beam types are: {[i.value for i in tbt.BeamType]}."
1013 )
1014 beam_type = tbt.BeamType(beam_set_db.get("type"))
1016 # detector settings
1017 detector_set_db = step_settings.get("detector")
1018 if detector_set_db is None:
1019 raise KeyError(
1020 f"Invalid .yml file, no 'detector' settings found in '{step_type}' step_type for step '{step_name}'."
1021 )
1022 auto_cb_set_db = detector_set_db.get("auto_cb")
1024 # scan settings
1025 scan_set_db = step_settings.get("scan")
1026 if scan_set_db is None:
1027 raise KeyError(
1028 f"Invalid .yml file, no 'scan' settings found in '{step_type}' step_type for step '{step_name}'."
1029 )
1030 scan_res = string_to_res(scan_set_db.get("resolution"))
1032 # misc settings
1033 bit_depth = step_settings.get("bit_depth")
1035 # TODO incorporate tile settings
1036 if yml_format.version >= 1.1: 1036 ↛ 1038, 1036 ↛ 10402 missed branches: 1) line 1036 didn't jump to line 1038 because the condition on line 1036 was never true, 2) line 1036 didn't jump to line 1040 because the condition on line 1036 was always true
1037 # tile settings
1038 tile_set_db = step_settings.get("tile_settings")
1040 validate_beam_settings(
1041 microscope=microscope,
1042 beam_type=beam_type,
1043 settings=beam_set_db,
1044 yml_format=yml_format,
1045 step_name=step_name,
1046 )
1047 beam_settings = tbt.BeamSettings(
1048 voltage_kv=beam_set_db["voltage_kv"],
1049 voltage_tol_kv=beam_set_db["voltage_tol_kv"],
1050 current_na=beam_set_db["current_na"],
1051 current_tol_na=beam_set_db["current_tol_na"],
1052 hfw_mm=beam_set_db["hfw_mm"],
1053 working_dist_mm=beam_set_db["working_dist_mm"],
1054 dynamic_focus=beam_set_db["dynamic_focus"],
1055 tilt_correction=beam_set_db["tilt_correction"],
1056 )
1058 validate_auto_cb_settings(
1059 settings=auto_cb_set_db,
1060 yml_format=yml_format,
1061 step_name=step_name,
1062 )
1063 auto_cb_settings = tbt.ScanArea(
1064 left=auto_cb_set_db["left"],
1065 top=auto_cb_set_db["top"],
1066 width=auto_cb_set_db["width"],
1067 height=auto_cb_set_db["height"],
1068 )
1070 validate_detector_settings(
1071 microscope=microscope,
1072 beam_type=beam_type,
1073 settings=detector_set_db,
1074 yml_format=yml_format,
1075 step_name=step_name,
1076 )
1077 detector_settings = tbt.Detector(
1078 type=tbt.DetectorType(detector_set_db["type"]),
1079 mode=tbt.DetectorMode(detector_set_db["mode"]),
1080 brightness=detector_set_db["brightness"],
1081 contrast=detector_set_db["contrast"],
1082 auto_cb_settings=auto_cb_settings,
1083 # custom_settings=None,
1084 )
1086 validate_scan_settings(
1087 microscope=microscope,
1088 beam_type=beam_type,
1089 settings=scan_set_db,
1090 yml_format=yml_format,
1091 step_name=step_name,
1092 )
1094 # cast resolution to preset if applicable
1095 if ut.valid_enum_entry(obj=scan_res, check_type=tbt.PresetResolution):
1096 scan_res = tbt.PresetResolution(scan_res)
1098 scan_settings = tbt.Scan(
1099 rotation_deg=scan_set_db["rotation_deg"],
1100 dwell_time_us=scan_set_db["dwell_time_us"],
1101 resolution=scan_res,
1102 # mode=tbt.ScanMode(scan_set_db["mode"]),
1103 )
1105 # make sure Scan rotation is 0 if dynamic focus or tilt correction is on (using auto mode only for angular correction)
1106 if beam_settings.dynamic_focus or beam_settings.tilt_correction: 1106 ↛ 1113line 1106 didn't jump to line 1113 because the condition on line 1106 was always true
1107 if not math.isclose(a=scan_settings.rotation_deg, b=0.0): 1107 ↛ 1113line 1107 didn't jump to line 1113 because the condition on line 1107 was always true
1108 raise ValueError(
1109 f"Invalid .yml for step '{step_name}'. Scan rotation of '{scan_settings.rotation_deg}' degrees requested with tilt_correction and/or dynamic focus set to 'True'. Cannot use dynamic focus or tilt correction for non-zero scan rotation."
1110 )
1112 # validate bit_depth
1113 if not ut.valid_enum_entry(bit_depth, tbt.ColorDepth):
1114 valid_bit_depths = [i.value for i in tbt.ColorDepth] 1114 ↛ exit, 1114 ↛ 11152 missed branches: 1) line 1114 didn't run the list comprehension on line 1114, 2) line 1114 didn't jump to line 1115 because
1115 raise ValueError(
1116 f"Unsupported bit depth of {bit_depth}, available depths are {valid_bit_depths}"
1117 )
1119 image_settings = tbt.ImageSettings(
1120 microscope=microscope,
1121 beam=beam_object_type(beam_type)(settings=beam_settings),
1122 detector=detector_settings,
1123 scan=scan_settings,
1124 bit_depth=bit_depth,
1125 )
1127 return image_settings
1130def fib(
1131 microscope: tbt.Microscope,
1132 step_settings: dict,
1133 step_name: str,
1134 yml_format: tbt.YMLFormatVersion,
1135) -> tbt.FIBSettings:
1136 """
1137 Convert a FIB step from a .yml file to microscope settings for performing a FIB operation.
1139 This function converts a FIB step from a .yml file to `FIBSettings` for the microscope. It performs schema checking to ensure valid inputs are requested.
1141 ## Parameters
1143 - `microscope` (`tbt.Microscope`): The microscope object for which to set the FIB settings.
1144 - `step_settings` (`dict`): The dictionary containing the FIB step settings from the .yml file.
1145 - `step_name` (`str`): The name of the step in the .yml file.
1146 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1148 ## Returns
1150 - `tbt.FIBSettings`: The FIB settings object.
1152 ## Raises
1154 - `KeyError`: If required settings are missing from the .yml file.
1155 - `ValueError`: If invalid beam type is requested.
1156 """
1157 ## create image_step_settings from this
1158 if yml_format.version >= 1.0: 1158 ↛ 1159, 1158 ↛ 11672 missed branches: 1) line 1158 didn't jump to line 1159 because the condition on line 1158 was never true, 2) line 1158 didn't jump to line 1167 because the condition on line 1158 was always true
1159 image_step_settings = step_settings.get("image")
1160 image_step_settings["step_general"] = step_settings.get("step_general")
1162 mill_step_settings = step_settings.get("mill")
1163 mill_beam_db = mill_step_settings.get("beam")
1164 mill_pattern_db = mill_step_settings.get("pattern")
1166 # ensure image is with an ion beam
1167 enforce_beam_type(
1168 tbt.IonBeam(settings=None),
1169 step_settings=image_step_settings,
1170 step_name=step_name,
1171 yml_format=yml_format,
1172 )
1173 image_settings = image(
1174 microscope=microscope,
1175 step_settings=image_step_settings,
1176 step_name=step_name,
1177 yml_format=yml_format,
1178 )
1180 ## fib mill settings
1181 # ensure milling is with an ion beam
1182 enforce_beam_type(
1183 tbt.IonBeam(settings=None),
1184 step_settings=mill_step_settings,
1185 step_name=step_name,
1186 yml_format=yml_format,
1187 )
1188 beam_type = tbt.BeamType(mill_beam_db.get("type"))
1189 # mill beam
1190 validate_beam_settings(
1191 microscope=microscope,
1192 beam_type=beam_type,
1193 settings=mill_beam_db,
1194 yml_format=yml_format,
1195 step_name=step_name,
1196 )
1197 mill_beam = tbt.IonBeam(
1198 settings=tbt.BeamSettings(
1199 voltage_kv=mill_beam_db["voltage_kv"],
1200 voltage_tol_kv=mill_beam_db["voltage_tol_kv"],
1201 current_na=mill_beam_db["current_na"],
1202 current_tol_na=mill_beam_db["current_tol_na"],
1203 hfw_mm=mill_beam_db["hfw_mm"],
1204 working_dist_mm=mill_beam_db["working_dist_mm"],
1205 dynamic_focus=mill_beam_db["dynamic_focus"],
1206 tilt_correction=mill_beam_db["tilt_correction"],
1207 )
1208 )
1210 # fib pattern settings
1211 pattern = validate_fib_pattern_settings(
1212 microscope=microscope,
1213 settings=mill_pattern_db,
1214 yml_format=yml_format,
1215 step_name=step_name,
1216 )
1218 fib_settings = tbt.FIBSettings(
1219 microscope=microscope,
1220 image=image_settings,
1221 mill_beam=mill_beam,
1222 pattern=pattern,
1223 )
1224 return fib_settings
1227@singledispatch
1228def enforce_beam_type(
1229 beam_type,
1230 step_settings: dict,
1231 step_name: str,
1232 yml_format: tbt.YMLFormatVersion,
1233) -> bool:
1234 """
1235 Enforce a specific beam type is used for an operation based on a dictionary.
1237 This function ensures that the specified beam type is used for an operation based on the provided settings dictionary. The dictionary must contain a sub-dictionary with the key 'beam'.
1239 ## Parameters
1241 - `beam_type` (`Any`): The beam type to enforce.
1242 - `step_settings` (`dict`): The dictionary containing the step settings.
1243 - `step_name` (`str`): The name of the step in the .yml file.
1244 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1246 ## Returns
1248 - `bool`: True if the beam type is enforced successfully.
1250 ## Raises
1252 - `NotImplementedError`: If no handler is available for the provided type.
1253 """
1254 _ = beam_type
1255 __ = step_settings
1256 ___ = step_name
1257 ____ = yml_format
1258 raise NotImplementedError(f"No handler for type {type(step_settings)}")
1261@enforce_beam_type.register
1262def _(
1263 beam_type: tbt.ElectronBeam,
1264 step_settings: dict,
1265 step_name: str,
1266 yml_format: tbt.YMLFormatVersion,
1267) -> bool:
1268 """
1269 Enforce that an electron beam is used for an operation based on a dictionary.
1271 This function ensures that an electron beam is used for an operation based on the provided settings dictionary.
1273 ## Parameters
1275 - `beam_type` (`tbt.ElectronBeam`): The electron beam type to enforce.
1276 - `step_settings` (`dict`): The dictionary containing the step settings.
1277 - `step_name` (`str`): The name of the step in the .yml file.
1278 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1280 ## Returns
1282 - `bool`: True if the electron beam type is enforced successfully.
1284 ## Raises
1286 - `KeyError`: If the 'beam' settings are missing from the .yml file.
1287 - `NotImplementedError`: If the beam type is unsupported or not an electron beam.
1288 """
1289 # beam must be electron
1290 if yml_format.version >= 1.0:
1291 beam_set_db = step_settings.get("beam")
1292 if beam_set_db is None: 1292 ↛ 1296line 1292 didn't jump to line 1296 because the condition on line 1292 was always true
1293 raise KeyError(
1294 f"Invalid .yml file, no 'beam' settings found in step '{step_name}'."
1295 )
1296 beam_type_value = beam_set_db.get("type")
1298 electron_beam_error_message = f"Unsupported beam type of '{beam_type_value}' in step '{step_name}'. '{tbt.BeamType.ELECTRON.value}' beam type must be used."
1299 if not ut.valid_enum_entry(beam_type_value, tbt.BeamType):
1300 raise NotImplementedError(electron_beam_error_message)
1301 if not tbt.BeamType(beam_type_value) == tbt.BeamType.ELECTRON:
1302 raise NotImplementedError(electron_beam_error_message)
1305@enforce_beam_type.register
1306def _(
1307 beam_type: tbt.IonBeam,
1308 step_settings: dict,
1309 step_name: str,
1310 yml_format: tbt.YMLFormatVersion,
1311) -> bool:
1312 """
1313 Enforce that an ion beam is used for an operation based on a dictionary.
1315 This function ensures that an ion beam is used for an operation based on the provided settings dictionary.
1317 ## Parameters
1319 - `beam_type` (`tbt.IonBeam`): The ion beam type to enforce.
1320 - `step_settings` (`dict`): The dictionary containing the step settings.
1321 - `step_name` (`str`): The name of the step in the .yml file.
1322 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1324 ## Returns
1326 - `bool`: True if the ion beam type is enforced successfully.
1328 ## Raises
1330 - `KeyError`: If the 'beam' settings are missing from the .yml file.
1331 - `NotImplementedError`: If the beam type is unsupported or not an ion beam.
1332 """
1333 # beam must be ion
1334 if yml_format.version >= 1.0:
1335 beam_set_db = step_settings.get("beam")
1336 if beam_set_db is None: 1336 ↛ 1337, 1336 ↛ 13402 missed branches: 1) line 1336 didn't jump to line 1337 because the condition on line 1336 was never true, 2) line 1336 didn't jump to line 1340 because the condition on line 1336 was always true
1337 raise KeyError(
1338 f"Invalid .yml file, no 'beam' settings found in step '{step_name}'."
1339 )
1340 beam_type_value = beam_set_db.get("type")
1342 ion_beam_error_message = f"Unsupported beam type of '{beam_type_value}' for step '{step_name}'. '{tbt.BeamType.ION.value}' beam type must be used."
1343 if not ut.valid_enum_entry(beam_type_value, tbt.BeamType):
1344 raise NotImplementedError(ion_beam_error_message)
1345 if not tbt.BeamType(beam_type_value) == tbt.BeamType.ION:
1346 raise NotImplementedError(ion_beam_error_message)
1349def ebsd(
1350 microscope: tbt.Microscope,
1351 step_settings: dict,
1352 step_name: str,
1353 yml_format: tbt.YMLFormatVersion,
1354) -> tbt.EBSDSettings:
1355 """
1356 Convert an EBSD step from a .yml file to microscope settings for performing an EBSD operation.
1358 This function converts an EBSD step from a .yml file to `EBSDSettings` for the microscope. It performs schema checking to ensure valid inputs are requested.
1360 ## Parameters
1362 - `microscope` (`tbt.Microscope`): The microscope object for which to set the EBSD settings.
1363 - `step_settings` (`dict`): The dictionary containing the EBSD step settings from the .yml file.
1364 - `step_name` (`str`): The name of the step in the .yml file.
1365 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1367 ## Returns
1369 - `tbt.EBSDSettings`: The EBSD settings object.
1371 ## Raises
1373 - `KeyError`: If required settings are missing from the .yml file or if the 'concurrent_EDS' key is invalid.
1374 """
1375 enforce_beam_type(
1376 tbt.ElectronBeam(settings=None),
1377 step_settings=step_settings,
1378 step_name=step_name,
1379 yml_format=yml_format,
1380 )
1381 image_settings = image(
1382 microscope=microscope,
1383 step_settings=step_settings,
1384 step_name=step_name,
1385 yml_format=yml_format,
1386 )
1387 concurrent_EDS = step_settings.get("concurrent_EDS")
1388 if (concurrent_EDS is None) or (not concurrent_EDS):
1389 enable_eds = False
1390 elif concurrent_EDS == True:
1391 enable_eds = True
1392 else:
1393 raise KeyError(
1394 f"Invalid .yml file, for step '{step_name}', an EBSD type step. 'concurrent_EDS' key is '{concurrent_EDS}' of type {type(concurrent_EDS)} but must be boolean (True/False) or of NoneType (null in .yml)."
1395 )
1397 ebsd_settings = tbt.EBSDSettings(
1398 image=image_settings,
1399 enable_eds=enable_eds,
1400 enable_ebsd=True,
1401 )
1402 return ebsd_settings
1405def eds(
1406 microscope: tbt.Microscope,
1407 step_settings: dict,
1408 step_name: str,
1409 yml_format: tbt.YMLFormatVersion,
1410) -> tbt.EDSSettings:
1411 """
1412 Convert an EDS step from a .yml file to microscope settings for performing an EDS operation.
1414 This function converts an EDS step from a .yml file to `EDSSettings` for the microscope. It performs schema checking to ensure valid inputs are requested.
1416 ## Parameters
1418 - `microscope` (`tbt.Microscope`): The microscope object for which to set the EDS settings.
1419 - `step_settings` (`dict`): The dictionary containing the EDS step settings from the .yml file.
1420 - `step_name` (`str`): The name of the step in the .yml file.
1421 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1423 ## Returns
1425 - `tbt.EDSSettings`: The EDS settings object.
1426 """
1427 enforce_beam_type(
1428 tbt.ElectronBeam(settings=None),
1429 step_settings=step_settings,
1430 step_name=step_name,
1431 yml_format=yml_format,
1432 )
1433 image_settings = image(
1434 microscope=microscope,
1435 step_settings=step_settings,
1436 step_name=step_name,
1437 yml_format=yml_format,
1438 )
1439 eds_settings = tbt.EDSSettings(
1440 image=image_settings,
1441 enable_eds=True,
1442 )
1443 return eds_settings
1446def custom(
1447 microscope: tbt.Microscope,
1448 step_settings: dict,
1449 step_name: str,
1450 yml_format: tbt.YMLFormatVersion,
1451) -> tbt.CustomSettings:
1452 """
1453 Convert a custom step from a .yml file to custom settings for the microscope.
1455 This function converts a custom step from a .yml file to `CustomSettings` for the microscope. It performs schema checking to ensure valid inputs are requested.
1457 ## Parameters
1459 - `microscope` (`tbt.Microscope`): The microscope object for which to set the custom settings.
1460 - `step_settings` (`dict`): The dictionary containing the custom step settings from the .yml file.
1461 - `step_name` (`str`): The name of the step in the .yml file.
1462 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1464 ## Returns
1466 - `tbt.CustomSettings`: The custom settings object.
1468 ## Raises
1470 - `KeyError`: If required settings are missing from the .yml file.
1471 - `ValueError`: If the specified script or executable path does not exist.
1472 """
1473 if yml_format.version >= 1.0:
1474 script_path = step_settings.get("script_path")
1475 if script_path is None:
1476 raise KeyError(
1477 f"Invalid .yml file, no 'script_path' found in custom step_type for step '{step_name}'."
1478 )
1479 if not Path(script_path).is_file():
1480 raise ValueError(
1481 f"Invalid location for script at location {script_path}. File does not exist."
1482 )
1484 executable_path = step_settings.get("executable_path")
1485 if executable_path is None: 1485 ↛ 1486, 1485 ↛ 14892 missed branches: 1) line 1485 didn't jump to line 1486 because the condition on line 1485 was never true, 2) line 1485 didn't jump to line 1489 because the condition on line 1485 was always true
1486 raise KeyError(
1487 f"Invalid .yml file, no 'executable_path' found in custom step_type for step '{step_name}'."
1488 )
1489 if not Path(executable_path).is_file():
1490 raise ValueError(
1491 f"Invalid location for executable at location {executable_path}. File does not exist."
1492 )
1494 custom_settings = tbt.CustomSettings(
1495 script_path=Path(script_path),
1496 executable_path=Path(executable_path),
1497 )
1498 return custom_settings
1501# def fib_pattern_type(
1502# settings: tbt.FIBSettings,
1503# ) -> Union[tbt.FIBBoxPattern, tbt.FIBStreamPattern]:
1504# """Returns specific pattern type settings for a properly formatted FIBSettings object"""
1507def scan_limits(
1508 selected_beam: property,
1509) -> tbt.ScanLimits:
1510 """
1511 Retrieve the scan settings limits for the selected beam.
1513 This function retrieves the limits for rotation and dwell time for the selected beam and returns them as a `ScanLimits` object.
1515 ## Parameters
1517 - `selected_beam` (`property`): The selected beam property from which to retrieve the scan limits.
1519 ## Returns
1521 - `tbt.ScanLimits`: The scan limits for rotation (degrees) and dwell time (microseconds).
1522 """
1523 # rotation
1524 min_deg = selected_beam.scanning.rotation.limits.min * Conversions.RAD_TO_DEG
1525 max_deg = selected_beam.scanning.rotation.limits.max * Conversions.RAD_TO_DEG
1526 # dwell_time
1527 min_dwell_us = selected_beam.scanning.dwell_time.limits.min * Conversions.S_TO_US
1528 max_dwell_us = selected_beam.scanning.dwell_time.limits.max * Conversions.S_TO_US
1530 return tbt.ScanLimits(
1531 rotation_deg=tbt.Limit(min=min_deg, max=max_deg),
1532 dwell_us=tbt.Limit(min=min_dwell_us, max=max_dwell_us),
1533 )
1536def string_to_res(input: str) -> tbt.Resolution:
1537 """
1538 Convert a string in the format "{{width}}x{{height}}" to a resolution object.
1540 This function takes a string representing the resolution in the format "WIDTHxHEIGHT" and converts it to a `Resolution` object.
1542 ## Parameters
1544 - `input` (`str`): The string representing the resolution in the format "WIDTHxHEIGHT".
1546 ## Returns
1548 - `tbt.Resolution`: The resolution object.
1550 ## Raises
1552 - `ValueError`: If the input string is not in the expected format.
1553 """
1554 try:
1555 split_res = (input.lower()).split("x")
1556 width, height = int(split_res[0]), int(split_res[1])
1557 except:
1558 raise ValueError(
1559 f"""Invalid string format,
1560 expected string format of "WIDTHxHEIGHT",
1561 but received the following: "{input}"."""
1562 )
1564 return tbt.Resolution(width=width, height=height)
1567def valid_string_resolution(string_resolution: str) -> bool:
1568 """
1569 Validate a string resolution.
1571 This function validates a string resolution by converting it to a `Resolution` object and checking if the width and height are within the specified limits.
1573 ## Parameters
1575 - `string_resolution` (`str`): The string representing the resolution in the format "WIDTHxHEIGHT".
1577 ## Returns
1579 - `bool`: True if the resolution is valid, False otherwise.
1580 """
1581 res = string_to_res(string_resolution)
1582 width, height = res.width, res.height
1583 return (
1584 ut.in_interval(
1585 width,
1586 limit=Constants.scan_resolution_limit,
1587 type=tbt.IntervalType.CLOSED,
1588 )
1589 ) and (
1590 ut.in_interval(
1591 height,
1592 limit=Constants.scan_resolution_limit,
1593 type=tbt.IntervalType.CLOSED,
1594 )
1595 )
1598def validate_auto_cb_settings(
1599 settings: dict,
1600 yml_format: tbt.YMLFormatVersion,
1601 step_name: str,
1602) -> bool:
1603 """
1604 Perform schema checking for auto contrast/brightness setting dictionary.
1606 This function validates the auto contrast/brightness settings dictionary based on the specified yml format.
1608 ## Parameters
1610 - `settings` (`dict`): The dictionary containing the auto contrast/brightness settings.
1611 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1612 - `step_name` (`str`): The name of the step in the .yml file.
1614 ## Returns
1616 - `bool`: True if the settings are valid, False otherwise.
1618 ## Raises
1620 - `KeyError`: If required keys are missing from the settings dictionary.
1621 - `ValueError`: If the settings do not satisfy the specified schema.
1622 """
1624 if ut.none_value_dictionary(settings): 1624 ↛ 1627line 1624 didn't jump to line 1627 because the condition on line 1624 was always true
1625 return True
1627 if settings.get("left") is None or settings.get("top") is None: 1627 ↛ 1636line 1627 didn't jump to line 1636 because the condition on line 1627 was always true
1628 if settings.get("left") is None: 1628 ↛ 1629, 1628 ↛ 16312 missed branches: 1) line 1628 didn't jump to line 1629 because the condition on line 1628 was never true, 2) line 1628 didn't jump to line 1631 because the condition on line 1628 was always true
1629 missing_key = "left"
1630 else:
1631 missing_key = "top"
1632 raise KeyError(
1633 f"Missing or no value for '{missing_key}' key in 'auto_cb' sub-dictionary in step '{step_name}' All 'auto_cb' sub-dictionary values must be declared by the user to implement this capability."
1634 )
1636 origin_limit = tbt.Limit(min=0.0, max=1.0) # reduced area limit for scan window
1637 width_limit = tbt.Limit(min=0.0, max=1.0 - settings["left"])
1638 height_limit = tbt.Limit(min=0.0, max=1.0 - settings["top"])
1640 if yml_format.version >= 1.0: 1640 ↛ 1683line 1640 didn't jump to line 1683 because the condition on line 1640 was always true
1641 schema = Schema( 1641 ↛ 1683line 1641 didn't jump to line 1683 because
1642 {
1643 "left": And(
1644 float,
1645 lambda x: ut.in_interval(
1646 x,
1647 limit=origin_limit,
1648 type=tbt.IntervalType.RIGHT_OPEN,
1649 ),
1650 error=f"In step '{step_name}', requested auto contrast/brightness window setting 'left' of '{settings['left']}' must satisfy '0 <= left < {origin_limit.max}'. Origin is in top-left corner of the field of view.",
1651 ),
1652 "top": And(
1653 float,
1654 lambda x: ut.in_interval(
1655 x,
1656 limit=origin_limit,
1657 type=tbt.IntervalType.RIGHT_OPEN,
1658 ),
1659 error=f"In step '{step_name}', requested auto contrast/brightness windows setting 'top' of '{settings['top']}' must satisfy '0.0 <= top < {origin_limit.max}'. Origin is in top-left corner of the field of view.",
1660 ),
1661 "width": And(
1662 float,
1663 lambda x: ut.in_interval(
1664 x,
1665 limit=width_limit,
1666 type=tbt.IntervalType.LEFT_OPEN,
1667 ),
1668 error=f"In step '{step_name}', requested auto contrast/brightness windows setting 'width' of {settings['width']} must satisfy '0.0 < width <= {width_limit.max}' with 'left' setting of {settings['left']} as total width ('left' + 'width') cannot exceed 1.0",
1669 ),
1670 "height": And(
1671 float,
1672 lambda x: ut.in_interval(
1673 x,
1674 limit=height_limit,
1675 type=tbt.IntervalType.LEFT_OPEN,
1676 ),
1677 error=f"In step '{step_name}', requested auto contrast/brightness windows setting 'height' of {settings['height']} must satisfy '0.0 < height <= {height_limit.max}' with 'top' setting of {settings['top']} as total height ('top' + 'height') cannot exceed 1.0",
1678 ),
1679 },
1680 ignore_extra_keys=True,
1681 )
1683 try:
1684 schema.validate(settings)
1685 except UnboundLocalError:
1686 raise ValueError(
1687 f"Error. Unsupported yml version {yml_format.version} provided."
1688 )
1689 return True
1692def validate_stage_position(
1693 microscope: tbt.Microscope,
1694 step_name: str,
1695 settings: dict,
1696 yml_format: tbt.YMLFormatVersion,
1697) -> bool:
1698 """
1699 Perform schema checking for stage position dictionary.
1701 This function validates the stage position settings dictionary based on the specified yml format and the stage limits of the microscope.
1703 ## Parameters
1705 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the stage position settings.
1706 - `step_name` (`str`): The name of the step in the .yml file.
1707 - `settings` (`dict`): The dictionary containing the stage position settings.
1708 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1710 ## Returns
1712 - `bool`: True if the settings are valid, False otherwise.
1714 ## Raises
1716 - `ValueError`: If the yml version is unsupported.
1717 """
1718 limits = stage_limits(microscope=microscope)
1720 if yml_format.version >= 1.0: 1720 ↛ 1772line 1720 didn't jump to line 1772 because the condition on line 1720 was always true
1721 schema = Schema( 1721 ↛ 1772line 1721 didn't jump to line 1772 because
1722 {
1723 "x_mm": And(
1724 float,
1725 lambda x: ut.in_interval(
1726 x,
1727 limit=limits.x_mm,
1728 type=tbt.IntervalType.CLOSED,
1729 ),
1730 error=f"Requested x-axis position of {settings['x_mm']} mm for step '{step_name}' must satisfy '{limits.x_mm.min} <= x_mm <= {limits.x_mm.max}'",
1731 ),
1732 "y_mm": And(
1733 float,
1734 lambda x: ut.in_interval(
1735 x,
1736 limit=limits.y_mm,
1737 type=tbt.IntervalType.CLOSED,
1738 ),
1739 error=f"Requested y-axis position of {settings['y_mm']} mm for step '{step_name}' must satisfy '{limits.y_mm.min} <= y_mm <= {limits.y_mm.max}'",
1740 ),
1741 "z_mm": And(
1742 float,
1743 lambda x: ut.in_interval(
1744 x,
1745 limit=limits.z_mm,
1746 type=tbt.IntervalType.CLOSED,
1747 ),
1748 error=f"Requested z-axis position of {settings['z_mm']} mm for step '{step_name}' must satisfy '{limits.z_mm.min} <= z_mm <= {limits.z_mm.max}'",
1749 ),
1750 "r_deg": And(
1751 float,
1752 lambda x: ut.in_interval(
1753 x,
1754 limit=limits.r_deg,
1755 type=tbt.IntervalType.RIGHT_OPEN,
1756 ),
1757 error=f"Requested r-axis position of {settings['r_deg']} degree for step '{step_name}' must satisfy '{limits.r_deg.min} <= r_deg < {limits.r_deg.max}'",
1758 ),
1759 "t_deg": And(
1760 float,
1761 lambda x: ut.in_interval(
1762 x,
1763 limit=limits.t_deg,
1764 type=tbt.IntervalType.CLOSED,
1765 ),
1766 error=f"Requested r-axis position of {settings['t_deg']} degree for step '{step_name}' must satisfy '{limits.t_deg.min} <= t_deg <= {limits.t_deg.max}'",
1767 ),
1768 },
1769 ignore_extra_keys=True,
1770 )
1772 try:
1773 schema.validate(settings)
1774 except UnboundLocalError:
1775 raise ValueError(
1776 f"Error. Unsupported yml version {yml_format.version} provided."
1777 )
1780def validate_beam_settings(
1781 microscope: tbt.Microscope,
1782 beam_type: tbt.BeamType,
1783 settings: dict,
1784 yml_format: tbt.YMLFormatVersion,
1785 step_name: str,
1786) -> bool:
1787 """
1788 Perform schema checking for beam setting dictionary.
1790 This function validates the beam settings dictionary based on the specified yml format and the beam limits of the microscope.
1792 ## Parameters
1794 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the beam settings.
1795 - `beam_type` (`tbt.BeamType`): The type of the beam (electron or ion).
1796 - `settings` (`dict`): The dictionary containing the beam settings.
1797 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1798 - `step_name` (`str`): The name of the step in the .yml file.
1800 ## Returns
1802 - `bool`: True if the settings are valid, False otherwise.
1804 ## Raises
1806 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
1807 """
1808 specified_beam = beam_object_type(beam_type)(settings=tbt.BeamSettings())
1809 selected_beam = ut.beam_type(specified_beam, microscope)
1811 limits = beam_limits(selected_beam, beam_type)
1813 if yml_format.version >= 1.0: 1813 ↛ 1897line 1813 didn't jump to line 1897 because the condition on line 1813 was always true
1814 schema = Schema(
1815 {
1816 "voltage_kv": And(
1817 float,
1818 lambda x: ut.in_interval(
1819 x,
1820 limit=limits.voltage_kv,
1821 type=tbt.IntervalType.CLOSED,
1822 ),
1823 error=f"In step '{step_name}', requested voltage of '{settings['voltage_kv']}' kV not within limits of {limits.voltage_kv.min} kV and {limits.voltage_kv.max} kV.",
1824 ),
1825 "voltage_tol_kv": And(
1826 float,
1827 lambda x: x > 0,
1828 error=f"In step '{step_name}', requested voltage tolerance of '{settings['voltage_tol_kv']}' kV must be a positive float (greater than 0).",
1829 ),
1830 "current_na": And(
1831 float,
1832 lambda x: ut.in_interval(
1833 x,
1834 limit=limits.current_na,
1835 type=tbt.IntervalType.CLOSED,
1836 ),
1837 error=f"In step '{step_name}', requested voltage of '{settings['current_na']}' nA not within limits of {limits.current_na.min} nA and {limits.current_na.max} nA",
1838 ),
1839 "current_tol_na": And(
1840 float,
1841 lambda x: x > 0,
1842 error=f"In step '{step_name}', 'current_tol_na' must be a positive float (greater than 0)",
1843 ),
1844 "hfw_mm": And(
1845 float,
1846 lambda x: ut.in_interval(
1847 x,
1848 limits.hfw_mm,
1849 type=tbt.IntervalType.CLOSED,
1850 ),
1851 error=f"In step '{step_name}', requested horizontal field width of '{settings['hfw_mm']}' mm not within limits of {limits.hfw_mm.min} mm and {limits.hfw_mm.max} mm",
1852 ),
1853 "working_dist_mm": And(
1854 float,
1855 lambda x: ut.in_interval(
1856 x,
1857 limit=limits.working_distance_mm,
1858 type=tbt.IntervalType.CLOSED,
1859 ),
1860 error=f"In step '{step_name}', requested working distance of '{settings['working_dist_mm']}' mm not within limits of {limits.working_distance_mm.min} mm and {limits.working_distance_mm.max} mm",
1861 ),
1862 },
1863 ignore_extra_keys=True,
1864 )
1866 e_beam_schema = Schema(
1867 {
1868 "dynamic_focus": Or(
1869 None,
1870 bool,
1871 error=f"In step '{step_name}' with 'electron' beam imaging, 'dynamic_focus' must be a boolean value but '{settings['dynamic_focus']}' of type {type(settings['dynamic_focus'])} was requested.",
1872 ),
1873 "tilt_correction": Or(
1874 None,
1875 bool,
1876 error=f"In step '{step_name}' with 'electron' beam imaging, 'tilt_correction' must be a boolean value but '{settings['tilt_correction']}' of type {type(settings['tilt_correction'])} was requested.",
1877 ),
1878 },
1879 ignore_extra_keys=True,
1880 )
1881 i_beam_schema = Schema(
1882 {
1883 "dynamic_focus": Or(
1884 None,
1885 False,
1886 error=f"In step '{step_name}' with 'ion' beam imaging, 'dynamic_focus' must be 'False' or 'None' but '{settings['dynamic_focus']}' of type {type(settings['dynamic_focus'])} was requested.",
1887 ),
1888 "tilt_correction": Or(
1889 None,
1890 False,
1891 error=f"In step '{step_name}' with 'ion' beam imaging, 'tilt_correction' must be 'False' or 'None' but '{settings['tilt_correction']}' of type {type(settings['tilt_correction'])} was requested.",
1892 ),
1893 },
1894 ignore_extra_keys=True,
1895 )
1897 try:
1898 schema.validate(settings)
1899 if specified_beam.type == tbt.BeamType.ELECTRON: 1899 ↛ 1901line 1899 didn't jump to line 1901 because the condition on line 1899 was always true
1900 e_beam_schema.validate(settings)
1901 if specified_beam.type == tbt.BeamType.ION: 1901 ↛ exitline 1901 didn't return from function 'validate_beam_settings' because the condition on line 1901 was always true
1902 i_beam_schema.validate(settings)
1903 except UnboundLocalError:
1904 raise ValueError(
1905 f"Error. Unsupported yml version {yml_format.version} provided."
1906 )
1909def validate_detector_settings(
1910 microscope: tbt.Microscope,
1911 beam_type: tbt.BeamType,
1912 settings: dict,
1913 yml_format: tbt.YMLFormatVersion,
1914 step_name: str,
1915) -> bool:
1916 """
1917 Perform schema checking for detector setting dictionary.
1919 This function validates the detector settings dictionary based on the specified yml format and the detector capabilities of the microscope.
1921 ## Parameters
1923 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the detector settings.
1924 - `beam_type` (`tbt.BeamType`): The type of the beam (electron or ion).
1925 - `settings` (`dict`): The dictionary containing the detector settings.
1926 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
1927 - `step_name` (`str`): The name of the step in the .yml file.
1929 ## Returns
1931 - `bool`: True if the settings are valid, False otherwise.
1933 ## Raises
1935 - `KeyError`: If auto contrast/brightness settings conflict with fixed brightness/contrast values.
1936 - `ValueError`: If the yml version is unsupported, or if the settings do not satisfy the specified schema, or if the detector type or mode is unsupported.
1937 """
1939 # switch to top left quad, enable e-beam
1940 devices.device_access(microscope=microscope)
1942 # set specified beam to active imaging device
1943 specified_beam = beam_object_type(beam_type)(settings=tbt.BeamSettings())
1944 img.set_beam_device(
1945 microscope=microscope,
1946 device=specified_beam.device,
1947 )
1949 auto_cb_db = settings.get("auto_cb")
1950 use_auto_cb = not ut.none_value_dictionary(settings.get("auto_cb"))
1951 if use_auto_cb and ( 1951 ↛ 1958line 1951 didn't jump to line 1958 because the condition on line 1951 was always true
1952 settings.get("brightness") is not None or settings.get("contrast") is not None
1953 ):
1954 raise KeyError(
1955 f"Auto contrast/brightness settings of {auto_cb_db} provided while settings of fixed brightness of '{settings.get('brightness')}' and fixed contrast of '{settings.get('contrast')}' were also provided. Users may use either fixed brightness/contrast values or auto contrast/brightness settings, but not both."
1956 )
1958 if yml_format.version >= 1.0: 1958 ↛ 1959, 1958 ↛ 19932 missed branches: 1) line 1958 didn't jump to line 1959 because the condition on line 1958 was never true, 2) line 1958 didn't jump to line 1993 because the condition on line 1958 was always true
1959 detector = settings.get("type")
1960 mode = settings.get("mode")
1961 cb_limit = tbt.Limit(min=0.0, max=1.0)
1962 schema = Schema( 1962 ↛ 1993line 1962 didn't jump to line 1993 because
1963 {
1964 "brightness": Or(
1965 None,
1966 And(
1967 float,
1968 lambda x: ut.in_interval(
1969 x,
1970 limit=cb_limit,
1971 type=tbt.IntervalType.CLOSED,
1972 ),
1973 ),
1974 error=f"In step '{step_name}', requested fixed brightness of '{settings['brightness']}'. This is not within limits of '>={cb_limit.min}' and '<={cb_limit.max}'.",
1975 ),
1976 "contrast": Or(
1977 None,
1978 And(
1979 float,
1980 lambda x: ut.in_interval(
1981 x,
1982 limit=cb_limit,
1983 type=tbt.IntervalType.CLOSED,
1984 ),
1985 ),
1986 error=f"In step '{step_name}', requested fixed contrast of '{settings['contrast']}'. This is not within limits of '>={cb_limit.min}' and '<={cb_limit.max}'.",
1987 ),
1988 },
1989 ignore_extra_keys=True,
1990 )
1992 # check detector type
1993 if not ut.valid_enum_entry(detector, tbt.DetectorType): 1993 ↛ 1994, 1993 ↛ 19972 missed branches: 1) line 1993 didn't jump to line 1994 because the condition on line 1993 was never true, 2) line 1993 didn't jump to line 1997 because the condition on line 1993 was always true
1994 raise ValueError(
1995 f"Unsupported detector type of '{detector}' on step '{step_name}'."
1996 )
1997 detector_type = tbt.DetectorType(detector)
1998 microscope_detector_types = available_detector_types(microscope=microscope)
1999 if detector_type.value not in microscope_detector_types:
2000 raise ValueError(
2001 f"Requested detector of {detector_type.value} is unavailable on this tool. Available detectors are: {microscope_detector_types}"
2002 )
2003 # make sure to set this detector as the active one to access modes
2004 img.detector_type(
2005 microscope=microscope,
2006 detector=detector_type,
2007 )
2009 # check detector mode
2010 if not ut.valid_enum_entry(mode, tbt.DetectorMode): 2010 ↛ 2011, 2010 ↛ 20142 missed branches: 1) line 2010 didn't jump to line 2011 because the condition on line 2010 was never true, 2) line 2010 didn't jump to line 2014 because the condition on line 2010 was always true
2011 raise ValueError(
2012 f'Unsupported detector mode of "{mode}" for "{detector}" detector'
2013 )
2014 detector_mode = tbt.DetectorMode(mode)
2015 microscope_detector_modes = available_detector_modes(microscope=microscope)
2016 if detector_mode.value not in microscope_detector_modes:
2017 raise ValueError(
2018 f"Requested mode of {detector_mode.value} for {detector_type.value} detector is invalid. Valid mode types are: {microscope_detector_modes}"
2019 )
2021 # check detector settings
2022 try:
2023 schema.validate(settings)
2024 except UnboundLocalError:
2025 raise ValueError(
2026 f"Error. Unsupported yml version {yml_format.version} provided."
2027 )
2030def validate_EBSD_EDS_settings(
2031 ebsd_oem: str,
2032 eds_oem: str,
2033) -> bool:
2034 """
2035 Check EBSD and EDS OEM and connection for supported OEMs.
2037 This function ensures that the specified EBSD and EDS OEMs are supported and that the connection settings are valid.
2039 ## Parameters
2041 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2042 - `connection_host` (`str`): The host for the microscope connection.
2043 - `connection_port` (`str`): The port for the microscope connection.
2044 - `ebsd_oem` (`str`): The OEM for the EBSD device.
2045 - `eds_oem` (`str`): The OEM for the EDS device.
2047 ## Returns
2049 - `bool`: True if the settings are valid, False otherwise.
2051 ## Raises
2053 - `NotImplementedError`: If differing EBSD and EDS OEMs are requested.
2054 - `ValueError`: If the EBSD or EDS OEM is unsupported.
2055 - `SystemError`: If the Laser API is not accessible.
2056 """
2057 # ensure same manufacturer for both EBSD and EDS
2058 if (ebsd_oem is not None) and (eds_oem is not None) and (ebsd_oem != eds_oem):
2059 raise NotImplementedError(
2060 f"Differing EBSD and EDS OEMs are not supported. Requested EBSD OEM of '{ebsd_oem}' and EDS OEM of '{eds_oem}'."
2061 )
2063 # Check EBSD OEM
2064 if not ut.valid_enum_entry(ebsd_oem, tbt.ExternalDeviceOEM): 2064 ↛ 2068line 2064 didn't jump to line 2068 because the condition on line 2064 was always true
2065 raise ValueError( 2065 ↛ exit, 2065 ↛ exit2 missed branches: 1) line 2065 didn't jump to the function exit, 2) line 2065 didn't except from function 'validate_EBSD_EDS_settings' because the raise on line 2065 wasn't executed
2066 f"Unsupported EBSD OEM of '{ebsd_oem}'. Supported OEM types are: {[i.value for i in tbt.ExternalDeviceOEM]}"
2067 )
2068 ebsd_device = tbt.ExternalDeviceOEM(ebsd_oem)
2069 # Check EDS OEM
2070 if not ut.valid_enum_entry(eds_oem, tbt.ExternalDeviceOEM):
2071 raise ValueError(
2072 f"Unsupported EDS OEM of '{eds_oem}'. Supported OEM types are: {[i.value for i in tbt.ExternalDeviceOEM]}"
2073 )
2074 eds_device = tbt.ExternalDeviceOEM(eds_oem)
2076 # exit if both devices are none, no need for laser control
2077 if ebsd_device == eds_device == tbt.ExternalDeviceOEM.NONE:
2078 return True
2080 # check EBSD and EDS connection
2081 try:
2082 fs_laser.tfs_laser
2083 except:
2084 raise SystemError(
2085 "EBSD and/or EDS control requested, but Laser API not accessible, so cannot use EBSD and EDS control. Please restart Laser API, or if not installed, change OEM to 'null', or leave blank in settings file."
2086 )
2088 ###################### PROPOSED CHANGE ######################
2089 # Retracting devices here is unnecessary
2090 # This causes devices to retract during validation
2091 # Device retraction is needed only when startin/ending experiments
2092 # Both of which are outside the scope of validation and already handled elsewhere
2093 ###################### PROPOSED CHANGE ######################
2094 # microscope = tbt.Microscope()
2095 # ut.connect_microscope(
2096 # microscope=microscope,
2097 # quiet_output=True,
2098 # connection_host=connection_host,
2099 # connection_port=connection_port,
2100 # )
2101 # if tbt.ExternalDeviceOEM(eds_oem) != tbt.ExternalDeviceOEM.NONE:
2102 # devices.retract_EDS(microscope=microscope)
2103 # if tbt.ExternalDeviceOEM(ebsd_oem) != tbt.ExternalDeviceOEM.NONE:
2104 # devices.retract_EBSD(microscope=microscope)
2105 # ut.disconnect_microscope(
2106 # microscope=microscope,
2107 # quiet_output=True,
2108 # )
2110 return True
2113def validate_general_settings(
2114 settings: dict,
2115 yml_format: tbt.YMLFormatVersion,
2116) -> bool:
2117 """
2118 Perform schema checking for general setting dictionary.
2120 This function validates the general settings dictionary based on the specified yml format. It checks the microscope connection and EBSD/EDS connection if valid OEMs are specified.
2122 ## Parameters
2124 - `settings` (`dict`): The dictionary containing the general settings.
2125 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2127 ## Returns
2129 - `bool`: True if the settings are valid, False otherwise.
2131 ## Raises
2133 - `ValueError`: If the general settings dictionary is empty, or if the settings do not satisfy the specified schema, or if the connection is invalid.
2134 - `NotImplementedError`: If the sectioning axis is unsupported.
2135 """
2137 if settings == {}: 2137 ↛ 2138, 2137 ↛ 21402 missed branches: 1) line 2137 didn't jump to line 2138 because the condition on line 2137 was never true, 2) line 2137 didn't jump to line 2140 because the condition on line 2137 was always true
2138 raise ValueError("General settings dictionary is empty.")
2140 slice_thickness_limit_um = Constants.slice_thickness_limit_um
2141 pre_tilt_limit_deg = Constants.pre_tilt_limit_deg_generic
2143 if yml_format.version >= 1.0:
2144 sectioning_axis = settings.get("sectioning_axis")
2145 connection_host = settings.get("connection_host")
2146 connection_port = settings.get("connection_port")
2147 ebsd_oem = settings.get("EBSD_OEM")
2148 eds_oem = settings.get("EDS_OEM")
2149 exp_dir = settings.get("exp_dir")
2150 h5_log_name = settings.get("h5_log_name")
2152 # Validate the non-numeric values
2153 # Check sectioning axis
2154 if not ut.valid_enum_entry(sectioning_axis, tbt.SectioningAxis): 2154 ↛ 2155, 2154 ↛ 21572 missed branches: 1) line 2154 didn't jump to line 2155 because the condition on line 2154 was never true, 2) line 2154 didn't jump to line 2157 because the condition on line 2154 was always true
2155 raise ValueError(f"Unsupported sectioning axis of {sectioning_axis}.")
2156 # TODO
2157 if tbt.SectioningAxis(sectioning_axis) != tbt.SectioningAxis.Z:
2158 raise NotImplementedError("Currently only Z-axis sectioning is supported.")
2159 if tbt.SectioningAxis(sectioning_axis) != tbt.SectioningAxis.Z:
2160 pre_tilt_limit_deg = Constants.pre_tilt_limit_deg_non_Z_sectioning # overwrite
2161 warnings.warn(
2162 "Pre-tilt value must be zero (0.0) degrees when using a sectioning axis other than 'Z'"
2163 )
2164 # Check connection host and port
2165 if not ut.valid_microscope_connection(
2166 host=connection_host,
2167 port=connection_port,
2168 ):
2169 raise ValueError(
2170 f"Unsupported connection with host of {connection_host} and port of {connection_port}."
2171 )
2173 # check EBSD and EDS
2174 validate_EBSD_EDS_settings(
2175 ebsd_oem=ebsd_oem,
2176 eds_oem=eds_oem,
2177 )
2179 # Check exp dir
2180 try:
2181 Path(exp_dir).mkdir(
2182 parents=True,
2183 exist_ok=True,
2184 )
2185 except TypeError:
2186 raise ValueError(
2187 f'Requested experimental directory of "{exp_dir}", which is not a valid path.'
2188 )
2189 # Check h5 log name
2190 if not isinstance(h5_log_name, str):
2191 raise ValueError(f'Unsupported h5 log name of "{h5_log_name}"')
2193 schema = Schema( 2193 ↛ 2237line 2193 didn't jump to line 2237 because
2194 {
2195 "slice_thickness_um": And(
2196 float,
2197 lambda x: ut.in_interval(
2198 x,
2199 limit=slice_thickness_limit_um,
2200 type=tbt.IntervalType.CLOSED,
2201 ),
2202 error=f"Requested slice thickness of {settings['slice_thickness_um']} um must satisfy '{slice_thickness_limit_um.min} <= slice_thickness_um <= {slice_thickness_limit_um.max}'",
2203 ),
2204 "max_slice_num": And(
2205 int,
2206 lambda x: x > 0,
2207 error=f"Requested max slice number of {settings['max_slice_num']} must satisfy '0 < max_slice_number'",
2208 ),
2209 "pre_tilt_deg": And(
2210 float,
2211 lambda x: ut.in_interval(
2212 x,
2213 limit=pre_tilt_limit_deg,
2214 type=tbt.IntervalType.CLOSED,
2215 ),
2216 error=f"Requested pre tilt of {settings['pre_tilt_deg']} degrees must satisfy '{pre_tilt_limit_deg.min} <= pre_tilt_deg <= {pre_tilt_limit_deg.max}'",
2217 ),
2218 "stage_translational_tol_um": And(
2219 float,
2220 lambda x: x > 0,
2221 error=f"Requested stage translational tolerance of {settings['stage_translational_tol_um']} um must be a positive float (greater than 0)",
2222 ),
2223 "stage_angular_tol_deg": And(
2224 float,
2225 lambda x: x > 0,
2226 error=f"Requested stage angular tolerance of {settings['stage_angular_tol_deg']} degrees must be a positive float (greater than 0)",
2227 ),
2228 "step_count": And(
2229 int,
2230 lambda x: x > 0,
2231 error=f"Requested step count of {settings['step_count']} must be a positive int (greater than 0)",
2232 ),
2233 },
2234 ignore_extra_keys=True,
2235 )
2237 try:
2238 schema.validate(settings)
2239 except UnboundLocalError:
2240 raise ValueError(
2241 f"Error. Unsupported yml version {yml_format.version} provided."
2242 )
2245def validate_scan_settings(
2246 microscope: tbt.Microscope,
2247 beam_type: tbt.BeamType,
2248 settings: dict,
2249 yml_format: tbt.YMLFormatVersion,
2250 step_name: str,
2251) -> bool:
2252 """
2253 Perform schema checking for scan setting dictionary.
2255 This function validates the scan settings dictionary based on the specified yml format and the scan limits of the microscope.
2257 ## Parameters
2259 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the scan settings.
2260 - `beam_type` (`tbt.BeamType`): The type of the beam (electron or ion).
2261 - `settings` (`dict`): The dictionary containing the scan settings.
2262 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2263 - `step_name` (`str`): The name of the step in the .yml file.
2265 ## Returns
2267 - `bool`: True if the settings are valid, False otherwise.
2269 ## Raises
2271 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
2272 """
2273 specified_beam = beam_object_type(beam_type)(settings=tbt.BeamSettings())
2274 selected_beam = ut.beam_type(specified_beam, microscope)
2276 """Schema checking for scan setting dictionary, format specified by yml_format"""
2277 if yml_format.version >= 1.0: 2277 ↛ 2284line 2277 didn't jump to line 2284 because the condition on line 2277 was always true
2278 limits = scan_limits(selected_beam)
2279 resolution = settings["resolution"]
2280 rotation_limit = limits.rotation_deg
2281 dwell_limit = limits.dwell_us
2283 # check resolution
2284 res = string_to_res(resolution)
2286 schema = Schema( 2286 ↛ 2315line 2286 didn't jump to line 2315 because
2287 {
2288 "resolution": And(
2289 str,
2290 lambda x: valid_string_resolution(x),
2291 error=f"In '{step_name}', resolution provided was {settings['resolution']}, but must be an integer width and height in format '[width]x[height]' with each dimension satisfying '{Constants.scan_resolution_limit.min} <= [value] <= {Constants.scan_resolution_limit.max}",
2292 ),
2293 "rotation_deg": And(
2294 float,
2295 lambda x: ut.in_interval(
2296 x,
2297 limit=rotation_limit,
2298 type=tbt.IntervalType.CLOSED,
2299 ),
2300 error=f"In '{step_name}', requested fixed rotation of '{settings['rotation_deg']}' degrees. This is not a float or not within limits of '>={rotation_limit.min}' and '<={rotation_limit.max}' degrees. Setting is of type {type(settings['rotation_deg'])}.",
2301 ),
2302 "dwell_time_us": And(
2303 float,
2304 lambda x: ut.in_interval(
2305 x,
2306 limit=dwell_limit,
2307 type=tbt.IntervalType.CLOSED,
2308 ),
2309 error=f"In '{step_name}', requested fixed dwell_time of '{settings['dwell_time_us']}' microseconds. This is a float or not within limits of '>={dwell_limit.min}' and '<={dwell_limit.max}' microseconds. Settting is of type {type(settings['dwell_time_us'])}.",
2310 ),
2311 },
2312 ignore_extra_keys=True,
2313 )
2315 try:
2316 schema.validate(settings)
2317 except UnboundLocalError:
2318 raise ValueError(
2319 f"Error. Unsupported yml version {yml_format.version} provided."
2320 )
2323def stage_position_settings(
2324 microscope: tbt.Microscope,
2325 step_name: str,
2326 general_settings: tbt.GeneralSettings,
2327 step_stage_settings: dict,
2328 yml_format: tbt.YMLFormatVersion,
2329) -> tbt.StageSettings:
2330 """
2331 Create a StagePositionUser object from settings, including validation.
2333 This function creates a `StagePositionUser` object from the provided settings and performs validation.
2335 ## Parameters
2337 - `microscope` (`tbt.Microscope`): The microscope object for which to set the stage position.
2338 - `step_name` (`str`): The name of the step in the .yml file.
2339 - `general_settings` (`tbt.GeneralSettings`): The general settings object.
2340 - `step_stage_settings` (`dict`): The dictionary containing the stage position settings for the step.
2341 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2343 ## Returns
2345 - `tbt.StageSettings`: The stage settings object.
2347 ## Raises
2349 - `NotImplementedError`: If the rotation side value is unsupported.
2350 - `ValueError`: If the stage position settings do not satisfy the specified schema.
2351 """
2353 if yml_format.version >= 1.0: 2353 ↛ 2354, 2353 ↛ 23622 missed branches: 1) line 2353 didn't jump to line 2354 because the condition on line 2353 was never true, 2) line 2353 didn't jump to line 2362 because the condition on line 2353 was always true
2354 pos_db = step_stage_settings.get("initial_position")
2356 rotation_side = step_stage_settings["rotation_side"]
2357 if not ut.valid_enum_entry(rotation_side, tbt.RotationSide):
2358 raise NotImplementedError(
2359 f"Unsupported rotation_side value of '{rotation_side}' of type '{type(rotation_side)}' in step '{step_name}', supported rotation_side values are: {[i.value for i in tbt.RotationSide]}."
2360 )
2362 validate_stage_position(
2363 microscope=microscope,
2364 step_name=step_name,
2365 settings=pos_db,
2366 yml_format=yml_format,
2367 )
2369 initial_position = tbt.StagePositionUser(
2370 x_mm=pos_db["x_mm"],
2371 y_mm=pos_db["y_mm"],
2372 z_mm=pos_db["z_mm"],
2373 r_deg=pos_db["r_deg"],
2374 t_deg=pos_db["t_deg"],
2375 )
2377 pretilt_angle_deg = general_settings.pre_tilt_deg
2378 sectioning_axis = general_settings.sectioning_axis
2380 stage_settings = tbt.StageSettings(
2381 microscope=microscope,
2382 initial_position=initial_position,
2383 pretilt_angle_deg=pretilt_angle_deg,
2384 sectioning_axis=sectioning_axis,
2385 rotation_side=tbt.RotationSide(rotation_side),
2386 # movement_mode=tbt.StageMovementMode.OUT_OF_PLANE,
2387 )
2389 return stage_settings
2392def validate_pulse_settings(
2393 settings: dict,
2394 yml_format: tbt.YMLFormatVersion,
2395 step_name: str,
2396) -> bool:
2397 """
2398 Perform schema checking for pulse setting dictionary.
2400 This function validates the pulse settings dictionary based on the specified yml format.
2402 ## Parameters
2404 - `settings` (`dict`): The dictionary containing the pulse settings.
2405 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2406 - `step_name` (`str`): The name of the step in the .yml file.
2408 ## Returns
2410 - `bool`: True if the settings are valid, False otherwise.
2412 ## Raises
2414 - `NotImplementedError`: If the wavelength or polarization value is unsupported.
2415 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
2416 """
2417 if yml_format.version >= 1.0: 2417 ↛ 2418, 2417 ↛ 24442 missed branches: 1) line 2417 didn't jump to line 2418 because the condition on line 2417 was never true, 2) line 2417 didn't jump to line 2444 because the condition on line 2417 was always true
2418 wavelength_nm = settings.get("wavelength_nm")
2419 if not ut.valid_enum_entry(wavelength_nm, tbt.LaserWavelength):
2420 raise NotImplementedError(
2421 f"In 'laser' step_type for step '{step_name}', unsupported wavelength of '{wavelength_nm}' nm (data type {type(wavelength_nm)}), supported wavelengths are: {[i.value for i in tbt.LaserWavelength]}."
2422 )
2423 polarization = settings.get("polarization")
2424 if not ut.valid_enum_entry(polarization, tbt.LaserPolarization):
2425 raise NotImplementedError(
2426 f"In 'laser' step_type for step '{step_name}', unsupported laser polarization of '{polarization}', supported values are: {[i.value for i in tbt.LaserPolarization]}."
2427 )
2429 schema = Schema( 2429 ↛ exit, 2429 ↛ 24442 missed branches: 1) line 2429 didn't jump to the function exit, 2) line 2429 didn't jump to line 2444 because
2430 {
2431 "divider": And(
2432 int,
2433 lambda x: x > 0,
2434 error=f"In 'laser' step_type for step '{step_name}', 'divider' parameter must be a positive integer greater than 0 but '{settings['divider']}' was requested.",
2435 ),
2436 "energy_uj": And(
2437 float,
2438 lambda x: x > 0,
2439 error=f"In 'laser' step_type for step '{step_name}', 'energy_uj' parameter must be a positive float greater than 0 but '{settings['energy_uj']}' was requested.",
2440 ),
2441 },
2442 ignore_extra_keys=True,
2443 )
2444 try:
2445 schema.validate(settings)
2446 except UnboundLocalError:
2447 raise ValueError(
2448 f"Error. Unsupported yml version {yml_format.version} provided."
2449 )
2452def validate_laser_optics_settings(
2453 settings: dict,
2454 yml_format: tbt.YMLFormatVersion,
2455 step_name: str,
2456) -> bool:
2457 """
2458 Perform schema checking for laser optics setting dictionary.
2460 This function validates the laser optics settings dictionary based on the specified yml format.
2462 ## Parameters
2464 - `settings` (`dict`): The dictionary containing the laser optics settings.
2465 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2466 - `step_name` (`str`): The name of the step in the .yml file.
2468 ## Returns
2470 - `bool`: True if the settings are valid, False otherwise.
2472 ## Raises
2474 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
2475 """
2476 if yml_format.version >= 1.0: 2476 ↛ 2499line 2476 didn't jump to line 2499 because the condition on line 2476 was always true
2477 schema = Schema( 2477 ↛ exit, 2477 ↛ 24992 missed branches: 1) line 2477 didn't jump to the function exit, 2) line 2477 didn't jump to line 2499 because
2478 {
2479 "objective_position_mm": And(
2480 float,
2481 lambda x: ut.in_interval(
2482 x,
2483 limit=Constants.laser_objective_limit_mm,
2484 type=tbt.IntervalType.CLOSED,
2485 ),
2486 error=f"In 'laser' step_type for step '{step_name}', 'objective_position_mm' parameter must be a float satisfying the following: {Constants.laser_objective_limit_mm.min} mm <= value <= {Constants.laser_objective_limit_mm.max} mm. '{settings['objective_position_mm']}' mm (of type {type(settings['objective_position_mm'])}) was requested.",
2487 ),
2488 "beam_shift_um_x": And(
2489 float,
2490 error=f"In 'laser' step_type for step '{step_name}', 'x' parameter for 'beam_shift_um' sub-dictioanry must be a float but '{settings['beam_shift_um_x']}' (of type {type(settings['beam_shift_um_x'])}) was requested.",
2491 ),
2492 "beam_shift_um_y": And(
2493 float,
2494 error=f"In 'laser' step_type for step '{step_name}', 'y' parameter for 'beam_shift_um' sub-dictioanry must be a float but '{settings['beam_shift_um_y']}' (of type {type(settings['beam_shift_um_y'])}) was requested.",
2495 ),
2496 },
2497 ignore_extra_keys=True,
2498 )
2499 try:
2500 schema.validate(settings)
2501 except UnboundLocalError:
2502 raise ValueError(
2503 f"Error. Unsupported yml version {yml_format.version} provided."
2504 )
2507def validate_laser_box_settings(
2508 settings: dict,
2509 yml_format: tbt.YMLFormatVersion,
2510 step_name: str,
2511) -> bool:
2512 """
2513 Perform schema checking for laser box pattern setting dictionary.
2515 This function validates the laser box pattern settings dictionary based on the specified yml format.
2517 ## Parameters
2519 - `settings` (`dict`): The dictionary containing the laser box pattern settings.
2520 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2521 - `step_name` (`str`): The name of the step in the .yml file.
2523 ## Returns
2525 - `bool`: True if the settings are valid, False otherwise.
2527 ## Raises
2529 - `NotImplementedError`: If the scan type or coordinate reference value is unsupported.
2530 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
2531 """
2532 if yml_format.version >= 1.0:
2533 # scan type
2534 scan_type = settings.get("scan_type")
2535 scan_types = [
2536 tbt.LaserScanType.RASTER,
2537 tbt.LaserScanType.SERPENTINE,
2538 ]
2539 scan_type_error_msg = f"In 'laser' step_type for step '{step_name}', unsupported scan type of '{scan_type}' for box pattern, supported scan types are: {[i.value for i in scan_types]}."
2540 if not ut.valid_enum_entry(scan_type, tbt.LaserScanType):
2541 raise NotImplementedError(scan_type_error_msg)
2542 if tbt.LaserScanType(scan_type) not in scan_types:
2543 raise NotImplementedError(scan_type_error_msg)
2545 # coordinate reference
2546 coordinate_ref = settings.get("coordinate_ref")
2547 coord_refs = [
2548 tbt.CoordinateReference.CENTER,
2549 tbt.CoordinateReference.UPPER_CENTER,
2550 tbt.CoordinateReference.UPPER_LEFT,
2551 ]
2552 coord_error_msg = f"In 'laser' step_type for step '{step_name}', unsupported coordinate reference of '{coordinate_ref}' for box pattern, supported coordinate references are: {[i.value for i in coord_refs]}." 2552 ↛ exitline 2552 didn't run the list comprehension on line 2552
2553 if not ut.valid_enum_entry(coordinate_ref, tbt.CoordinateReference):
2554 raise NotImplementedError(coord_error_msg)
2555 if tbt.CoordinateReference(coordinate_ref) not in coord_refs:
2556 raise NotImplementedError(coord_error_msg)
2558 schema = Schema( 2558 ↛ 2588line 2558 didn't jump to line 2588 because
2559 {
2560 "passes": And(
2561 int,
2562 lambda x: x > 0,
2563 error=f"In 'laser' step_type for step '{step_name}', 'passes' parameter in 'box' type pattern must be a positive integer. '{settings['passes']}' (of type {type(settings['passes'])}) was requested.",
2564 ),
2565 "size_x_um": And(
2566 float,
2567 lambda x: x > 0,
2568 error=f"In 'laser' step_type for step '{step_name}', 'size_x_um' parameter in 'box' type pattern must be a positive float. '{settings['size_x_um']}' mm (of type {type(settings['size_x_um'])}) was requested.",
2569 ),
2570 "size_y_um": And(
2571 float,
2572 lambda x: x > 0,
2573 error=f"In 'laser' step_type for step '{step_name}', 'size_y_um' parameter in 'box' type pattern must be a positive float. '{settings['size_y_um']}' mm (of type {type(settings['size_y_um'])}) was requested.",
2574 ),
2575 "pitch_x_um": And(
2576 float,
2577 lambda x: x > 0,
2578 error=f"In 'laser' step_type for step '{step_name}', 'pitch_x_um' parameter in 'box' type pattern must be a positive float. '{settings['pitch_x_um']}' mm (of type {type(settings['pitch_x_um'])}) was requested.",
2579 ),
2580 "pitch_y_um": And(
2581 float,
2582 lambda x: x > 0,
2583 error=f"In 'laser' step_type for step '{step_name}', 'pitch_y_um' parameter in 'box' type pattern must be a positive float. '{settings['pitch_y_um']}' mm (of type {type(settings['pitch_y_um'])}) was requested.",
2584 ),
2585 },
2586 ignore_extra_keys=True,
2587 )
2588 try:
2589 schema.validate(settings)
2590 except UnboundLocalError:
2591 raise ValueError(
2592 f"Error. Unsupported yml version {yml_format.version} provided."
2593 )
2596def validate_laser_line_settings(
2597 settings: dict,
2598 yml_format: tbt.YMLFormatVersion,
2599 step_name: str,
2600) -> bool:
2601 """
2602 Perform schema checking for laser line pattern setting dictionary.
2604 This function validates the laser line pattern settings dictionary based on the specified yml format.
2606 ## Parameters
2608 - `settings` (`dict`): The dictionary containing the laser line pattern settings.
2609 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2610 - `step_name` (`str`): The name of the step in the .yml file.
2612 ## Returns
2614 - `bool`: True if the settings are valid, False otherwise.
2616 ## Raises
2618 - `NotImplementedError`: If the scan type value is unsupported.
2619 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
2620 """
2621 if yml_format.version >= 1.0:
2622 # scan type
2623 scan_type = settings.get("scan_type")
2624 scan_types = [
2625 tbt.LaserScanType.SINGLE,
2626 tbt.LaserScanType.LAP,
2627 ]
2628 scan_type_error_msg = f"In 'laser' step_type for step '{step_name}', unsupported scan type of '{scan_type}' for line pattern, supported scan types are: {[i.value for i in scan_types]}."
2629 if not ut.valid_enum_entry(scan_type, tbt.LaserScanType):
2630 raise NotImplementedError(scan_type_error_msg)
2631 if tbt.LaserScanType(scan_type) not in scan_types:
2632 raise NotImplementedError(scan_type_error_msg)
2634 schema = Schema( 2634 ↛ exit, 2634 ↛ 26542 missed branches: 1) line 2634 didn't jump to the function exit, 2) line 2634 didn't jump to line 2654 because
2635 {
2636 "passes": And(
2637 int,
2638 lambda x: x > 0,
2639 error=f"In 'laser' step_type for step '{step_name}', 'passes' parameter in 'line' type pattern must be a positive integer. '{settings['passes']}' (of type {type(settings['passes'])}) was requested.",
2640 ),
2641 "size_um": And(
2642 float,
2643 lambda x: x > 0,
2644 error=f"In 'laser' step_type for step '{step_name}', 'size_um' parameter in 'line' type pattern must be a positive float. '{settings['size_um']}' mm (of type {type(settings['size_um'])}) was requested.",
2645 ),
2646 "pitch_um": And(
2647 float,
2648 lambda x: x > 0,
2649 error=f"In 'laser' step_type for step '{step_name}', 'pitch_um' parameter in 'line' type pattern must be a positive float. '{settings['pitch_um']}' mm (of type {type(settings['pitch_um'])}) was requested.",
2650 ),
2651 },
2652 ignore_extra_keys=True,
2653 )
2654 try:
2655 schema.validate(settings)
2656 except UnboundLocalError:
2657 raise ValueError(
2658 f"Error. Unsupported yml version {yml_format.version} provided."
2659 )
2662def validate_laser_mode_settings(
2663 settings: dict,
2664 yml_format: tbt.YMLFormatVersion,
2665 step_name: str,
2666) -> bool:
2667 """
2668 Perform schema checking for laser mode setting dictionary.
2670 This function validates the laser mode settings dictionary based on the specified yml format.
2672 ## Parameters
2674 - `settings` (`dict`): The dictionary containing the laser mode settings.
2675 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2676 - `step_name` (`str`): The name of the step in the .yml file.
2678 ## Returns
2680 - `bool`: True if the settings are valid, False otherwise.
2682 ## Raises
2684 - `NotImplementedError`: If the laser pattern mode value is unsupported.
2685 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
2686 """
2687 if yml_format.version >= 1.0:
2688 mode = settings.get("mode")
2689 if not ut.valid_enum_entry(mode, tbt.LaserPatternMode):
2690 raise NotImplementedError(
2691 f"In 'laser' step_type for step '{step_name}', unsupported laser pattern mode of '{mode}', supported values are: {[i.value for i in tbt.LaserPatternMode]}."
2692 )
2694 schema = Schema(
2695 {
2696 "rotation_deg": And(
2697 float,
2698 error=f"In 'laser' step_type for step '{step_name}', 'rotation_deg' parameter must be a float. '{settings['rotation_deg']}' degrees (of type {type(settings['rotation_deg'])}) was requested.",
2699 ),
2700 },
2701 ignore_extra_keys=True,
2702 )
2703 schema_fine = Schema( 2703 ↛ exitline 2703 didn't jump to the function exit
2704 {
2705 "pixel_dwell_ms": Or(
2706 None,
2707 "null",
2708 "None",
2709 error=f"In 'laser' step_type for step '{step_name}', 'pixel_dwell_ms' parameter does not apply to the selected 'fine' milling mode. Set 'pixel_dwell_ms' to 'null' 'None' or leave the entry blank to continue.",
2710 ),
2711 "pulses_per_pixel": And(
2712 int,
2713 lambda x: x > 0,
2714 error=f"In 'laser' step_type for step '{step_name}', 'pulses_per_pixel' parameter is required for pattern type of 'fine'. 'pulses_per_pixel' must be a positive integer greater than 0. '{settings['pulses_per_pixel']}' (of type {type(settings['pulses_per_pixel'])}) was requested.",
2715 ),
2716 },
2717 ignore_extra_keys=True,
2718 )
2719 schema_coarse = Schema( 2719 ↛ exit, 2719 ↛ 27352 missed branches: 1) line 2719 didn't jump to the function exit, 2) line 2719 didn't jump to line 2735 because
2720 {
2721 "pixel_dwell_ms": And(
2722 float,
2723 lambda x: x > 0.0,
2724 error=f"In 'laser' step_type for step '{step_name}', 'pixel_dwell_ms' parameter is required for pattern type of 'coarse'. 'pixel_dwell_ms' must be a positive float greater than 0. '{settings['pixel_dwell_ms']}' (of type {type(settings['pixel_dwell_ms'])}) was requested.",
2725 ),
2726 "pulses_per_pixel": Or(
2727 None,
2728 "null",
2729 "None",
2730 error=f"In 'laser' step_type for step '{step_name}', 'pulses_per_pixel' parameter does not apply to the selected 'coarse' milling mode. Set 'pulses_per_pixel' to 'null' 'None' or leave the entry blank to continue.",
2731 ),
2732 },
2733 ignore_extra_keys=True,
2734 )
2735 try:
2736 schema.validate(settings)
2737 if mode == "fine": 2737 ↛ 2739line 2737 didn't jump to line 2739 because the condition on line 2737 was always true
2738 schema_fine.validate(settings)
2739 if mode == "coarse":
2740 schema_coarse.validate(settings)
2741 except UnboundLocalError:
2742 raise ValueError(
2743 f"Error. Unsupported yml version {yml_format.version} provided."
2744 )
2747def validate_laser_pattern_settings(
2748 settings: dict,
2749 yml_format: tbt.YMLFormatVersion,
2750 step_name: str,
2751) -> tbt.LaserPatternType:
2752 """
2753 Perform schema checking for laser pattern setting dictionary.
2755 This function validates the laser pattern settings dictionary based on the specified yml format and determines the type of pattern (box or line).
2757 ## Parameters
2759 - `settings` (`dict`): The dictionary containing the laser pattern settings.
2760 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2761 - `step_name` (`str`): The name of the step in the .yml file.
2763 ## Returns
2765 - `tbt.LaserPatternType`: The type of the laser pattern (box or line).
2767 ## Raises
2769 - `KeyError`: If required settings are missing from the .yml file or if multiple pattern types are specified.
2770 - `ValueError`: If the laser pattern settings are invalid.
2771 """
2772 if yml_format.version >= 1.0: 2772 ↛ exit, 2772 ↛ 27742 missed branches: 1) line 2772 didn't return from function 'validate_laser_pattern_settings' because the condition on line 2772 was always true, 2) line 2772 didn't jump to line 2774 because the condition on line 2772 was never true
2773 # determine type of pattern (only one allowed)
2774 type_set_db = settings.get("type")
2775 if type_set_db is None:
2776 raise KeyError(
2777 f"Invalid .yml file, no 'type' settings sub-dictionary found in 'pattern' settings in 'laser' step_type for step '{step_name}'."
2778 )
2780 box_settings = type_set_db.get("box")
2781 line_settings = type_set_db.get("line")
2782 if box_settings is None:
2783 box_pattern = False
2784 else:
2785 box_pattern = not ut.none_value_dictionary(box_settings)
2786 if line_settings is None: 2786 ↛ 2789line 2786 didn't jump to line 2789 because the condition on line 2786 was always true
2787 line_pattern = False
2788 else:
2789 line_pattern = not ut.none_value_dictionary(line_settings)
2790 if box_pattern == line_pattern == False:
2791 raise KeyError(
2792 f"Invalid .yml file in 'laser' step_type for step '{step_name}'. No pattern type settings found."
2793 )
2794 if box_pattern == line_pattern == True: 2794 ↛ 2798line 2794 didn't jump to line 2798 because the condition on line 2794 was always true
2795 raise KeyError(
2796 f"Invalid .yml file in 'laser' step_type for step '{step_name}'. Pattern settings for one and only one type are allowed. Type settings found for both 'box' and 'line' type. Please leave one set of type settings completely blank, enter 'null' for each parameter, or remove the unused subdictionary completely from the .yml file."
2797 )
2798 validate_laser_mode_settings(
2799 settings=settings,
2800 yml_format=yml_format,
2801 step_name=step_name,
2802 )
2804 if box_pattern:
2805 validate_laser_box_settings(
2806 settings=box_settings,
2807 yml_format=yml_format,
2808 step_name=step_name,
2809 )
2810 return tbt.LaserPatternType.BOX
2811 elif line_pattern: 2811 ↛ 2812, 2811 ↛ 28192 missed branches: 1) line 2811 didn't jump to line 2812 because the condition on line 2811 was never true, 2) line 2811 didn't jump to line 2819 because the condition on line 2811 was always true
2812 validate_laser_line_settings(
2813 settings=line_settings,
2814 yml_format=yml_format,
2815 step_name=step_name,
2816 )
2817 return tbt.LaserPatternType.LINE
2818 else:
2819 raise ValueError( 2819 ↛ exit, 2819 ↛ exit2 missed branches: 1) line 2819 didn't jump to the function exit, 2) line 2819 didn't except from function 'validate_laser_pattern_settings' because the raise on line 2819 wasn't executed
2820 f"Invalid laser pattern settings for step {step_name}. Supported types are {[i.value for i in tbt.LaserPatternType]}"
2821 )
2824def validate_fib_pattern_settings(
2825 microscope: tbt.Microscope,
2826 settings: dict,
2827 yml_format: tbt.YMLFormatVersion,
2828 step_name: str,
2829) -> Union[
2830 tbt.FIBRectanglePattern,
2831 tbt.FIBRegularCrossSection,
2832 tbt.FIBCleaningCrossSection,
2833 tbt.FIBStreamPattern,
2834]:
2835 """
2836 Perform schema checking for FIB pattern setting dictionary.
2838 This function validates the FIB pattern settings dictionary based on the specified yml format and determines the type of pattern (rectangle, regular cross section, cleaning cross section, or selected area).
2840 ## Parameters
2842 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the FIB pattern settings.
2843 - `settings` (`dict`): The dictionary containing the FIB pattern settings.
2844 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
2845 - `step_name` (`str`): The name of the step in the .yml file.
2847 ## Returns
2849 - `Union[tbt.FIBRectanglePattern, tbt.FIBRegularCrossSection, tbt.FIBCleaningCrossSection, tbt.FIBStreamPattern]`: The validated FIB pattern object.
2851 ## Raises
2853 - `KeyError`: If required settings are missing from the .yml file or if multiple pattern types are specified.
2854 - `ValueError`: If the application file is unsupported or invalid for the specified pattern type.
2855 """
2857 if yml_format.version >= 1.0: 2857 ↛ 2858, 2857 ↛ 29172 missed branches: 1) line 2857 didn't jump to line 2858 because the condition on line 2857 was never true, 2) line 2857 didn't jump to line 2917 because the condition on line 2857 was always true
2858 application_file = settings.get("application_file")
2859 # determine type of pattern (only one allowed)
2860 type_set_db = settings.get("type")
2861 if type_set_db is None: 2861 ↛ 2862, 2861 ↛ 28652 missed branches: 1) line 2861 didn't jump to line 2862 because the condition on line 2861 was never true, 2) line 2861 didn't jump to line 2865 because the condition on line 2861 was always true
2862 raise KeyError(
2863 f"Invalid .yml file, no 'type' settings sub-dictionary found in 'pattern' settings in 'fib' step_type for step '{step_name}'."
2864 )
2865 rectangle_settings = type_set_db.get("rectangle")
2866 regular_cross_section_settings = type_set_db.get("regular_cross_section")
2867 cleaning_cross_section_settings = type_set_db.get("cleaning_cross_section")
2868 selected_area_settings = type_set_db.get("selected_area")
2870 pattern_settings = [
2871 rectangle_settings,
2872 regular_cross_section_settings,
2873 cleaning_cross_section_settings,
2874 selected_area_settings,
2875 ]
2876 (
2877 rectangle_pattern,
2878 regular_cross_section_pattern,
2879 cleaning_cross_section_pattern,
2880 selected_area_pattern,
2881 ) = (None, None, None, None)
2882 # pattern_names = [val.value for val in tbt.FIBPatternType] # remembers order
2883 # assert pattern_names == [
2884 # "rectangle",
2885 # "regular_cross_section",
2886 # "cleaning_cross_section",
2887 # "selected_area",
2888 # ]
2889 pattern_names = [
2890 "rectangle",
2891 "regular_cross_section",
2892 "cleaning_cross_section",
2893 "selected_area",
2894 ]
2895 pattern_types = [
2896 rectangle_pattern,
2897 regular_cross_section_pattern,
2898 cleaning_cross_section_pattern,
2899 selected_area_pattern,
2900 ]
2902 for type in range(len(pattern_settings)):
2903 db = pattern_settings[type]
2904 if db is None: 2904 ↛ 2905, 2904 ↛ 29072 missed branches: 1) line 2904 didn't jump to line 2905 because the condition on line 2904 was never true, 2) line 2904 didn't jump to line 2907 because the condition on line 2904 was always true
2905 pattern_types[type] = False
2906 else:
2907 pattern_types[type] = not ut.none_value_dictionary(db)
2909 # one and only one type of pattern can have settings
2910 if sum(pattern_types) != 1:
2911 raise KeyError( 2911 ↛ exit, 2911 ↛ exit2 missed branches: 1) line 2911 didn't jump to the function exit, 2) line 2911 didn't except from function 'validate_fib_pattern_settings' because the raise on line 2911 wasn't executed
2912 f"Invalid .yml file in 'fib' step_type for step '{step_name}'. Pattern settings for one and only one type are allowed. Please provide settings for only one of the supported pattern types: {[name for name in pattern_names]}. For unused pattern types, leave the type settings completely blank, enter 'null' for each parameter, or remove the unused subdictionary completely from the .yml file."
2913 )
2914 pattern_type = tbt.FIBPatternType(pattern_names[pattern_types.index(True)])
2916 # check application file
2917 valid_applications = application_files(microscope=microscope)
2918 # TODO make this print out pretty
2919 # display_applications = ",".join(valid_applications)
2920 # display_applications = ut.tabular_list(data=valid_applications)
2921 if application_file not in valid_applications: 2921 ↛ 2926line 2921 didn't jump to line 2926 because the condition on line 2921 was always true
2922 raise ValueError(
2923 f"Unsupported FIB application file of '{application_file}' on step '{step_name}. Supported applications on this microscope are: \n{valid_applications}'"
2924 )
2926 if pattern_type == tbt.FIBPatternType.RECTANGLE: 2926 ↛ 2969line 2926 didn't jump to line 2969 because the condition on line 2926 was always true
2927 validate_fib_box_settings(
2928 settings=rectangle_settings,
2929 yml_format=yml_format,
2930 step_name=step_name,
2931 pattern_type=pattern_type,
2932 )
2934 pattern = tbt.FIBPattern(
2935 application=application_file,
2936 type=pattern_type,
2937 geometry=tbt.FIBRectanglePattern(
2938 center_um=tbt.Point(
2939 x=rectangle_settings.get("center").get("x_um"),
2940 y=rectangle_settings.get("center").get("y_um"),
2941 ),
2942 width_um=rectangle_settings.get("width_um"),
2943 height_um=rectangle_settings.get("height_um"),
2944 depth_um=rectangle_settings.get("depth_um"),
2945 scan_direction=tbt.FIBPatternScanDirection(
2946 rectangle_settings.get("scan_direction")
2947 ),
2948 scan_type=tbt.FIBPatternScanType(rectangle_settings.get("scan_type")),
2949 ),
2950 )
2951 # make sure application file is valid for this pattern type:
2952 try:
2953 geometry = pattern.geometry
2954 microscope.patterning.set_default_application_file(pattern.application)
2955 microscope.patterning.create_rectangle(
2956 center_x=geometry.center_um.x * Conversions.UM_TO_M,
2957 center_y=geometry.center_um.y * Conversions.UM_TO_M,
2958 width=geometry.width_um * Conversions.UM_TO_M,
2959 height=geometry.height_um * Conversions.UM_TO_M,
2960 depth=geometry.depth_um * Conversions.UM_TO_M,
2961 )
2962 microscope.patterning.clear_patterns()
2963 except:
2964 raise ValueError(
2965 f'Invalid application file of "{pattern.application}" for Rectangle pattern type. Please select or create an appropriate application file.'
2966 )
2968 return pattern
2969 elif pattern_type == tbt.FIBPatternType.REGULAR_CROSS_SECTION:
2970 validate_fib_box_settings(
2971 settings=regular_cross_section_settings,
2972 yml_format=yml_format,
2973 step_name=step_name,
2974 pattern_type=pattern_type,
2975 )
2976 pattern = tbt.FIBPattern(
2977 application=application_file,
2978 type=pattern_type,
2979 geometry=tbt.FIBRegularCrossSection(
2980 center_um=tbt.Point(
2981 x=regular_cross_section_settings.get("center").get("x_um"),
2982 y=regular_cross_section_settings.get("center").get("y_um"),
2983 ),
2984 width_um=regular_cross_section_settings.get("width_um"),
2985 height_um=regular_cross_section_settings.get("height_um"),
2986 depth_um=regular_cross_section_settings.get("depth_um"),
2987 scan_direction=tbt.FIBPatternScanDirection(
2988 regular_cross_section_settings.get("scan_direction")
2989 ),
2990 scan_type=tbt.FIBPatternScanType(
2991 regular_cross_section_settings.get("scan_type")
2992 ),
2993 ),
2994 )
2995 # make sure application file is valid for this pattern type:
2996 try:
2997 geometry = pattern.geometry
2998 microscope.patterning.set_default_application_file(pattern.application)
2999 microscope.patterning.create_regular_cross_section(
3000 center_x=geometry.center_um.x * Conversions.UM_TO_M,
3001 center_y=geometry.center_um.y * Conversions.UM_TO_M,
3002 width=geometry.width_um * Conversions.UM_TO_M,
3003 height=geometry.height_um * Conversions.UM_TO_M,
3004 depth=geometry.depth_um * Conversions.UM_TO_M,
3005 )
3006 microscope.patterning.clear_patterns()
3007 except:
3008 raise ValueError(
3009 f'Invalid application file of "{pattern.application}" for Regular Cross Section pattern type. Please select or create an appropriate application file.'
3010 )
3011 return pattern
3012 elif pattern_type == tbt.FIBPatternType.CLEANING_CROSS_SECTION:
3013 validate_fib_box_settings(
3014 settings=cleaning_cross_section_settings,
3015 yml_format=yml_format,
3016 step_name=step_name,
3017 pattern_type=pattern_type,
3018 )
3019 pattern = tbt.FIBPattern(
3020 application=application_file,
3021 type=pattern_type,
3022 geometry=tbt.FIBCleaningCrossSection(
3023 center_um=tbt.Point(
3024 x=cleaning_cross_section_settings.get("center").get("x_um"),
3025 y=cleaning_cross_section_settings.get("center").get("y_um"),
3026 ),
3027 width_um=cleaning_cross_section_settings.get("width_um"),
3028 height_um=cleaning_cross_section_settings.get("height_um"),
3029 depth_um=cleaning_cross_section_settings.get("depth_um"),
3030 scan_direction=tbt.FIBPatternScanDirection(
3031 cleaning_cross_section_settings.get("scan_direction")
3032 ),
3033 scan_type=tbt.FIBPatternScanType(
3034 cleaning_cross_section_settings.get("scan_type")
3035 ),
3036 ),
3037 )
3038 # make sure application file is valid for this pattern type:
3039 try:
3040 geometry = pattern.geometry
3041 microscope.patterning.set_default_application_file(pattern.application)
3042 microscope.patterning.create_cleaning_cross_section(
3043 center_x=geometry.center_um.x * Conversions.UM_TO_M,
3044 center_y=geometry.center_um.y * Conversions.UM_TO_M,
3045 width=geometry.width_um * Conversions.UM_TO_M,
3046 height=geometry.height_um * Conversions.UM_TO_M,
3047 depth=geometry.depth_um * Conversions.UM_TO_M,
3048 )
3049 microscope.patterning.clear_patterns()
3050 except:
3051 raise ValueError(
3052 f'Invalid application file of "{pattern.application}" for Cleaning Cross Section pattern type. Please select or create an appropriate application file.'
3053 )
3054 return pattern
3055 elif pattern_type == tbt.FIBPatternType.SELECTED_AREA:
3056 validate_fib_selected_area_settings(
3057 settings=selected_area_settings,
3058 yml_format=yml_format,
3059 step_name=step_name,
3060 pattern_type=pattern_type,
3061 )
3062 pattern = tbt.FIBPattern(
3063 application=application_file,
3064 type=pattern_type,
3065 geometry=tbt.FIBStreamPattern(
3066 dwell_us=selected_area_settings.get("dwell_us"),
3067 repeats=selected_area_settings.get("repeats"),
3068 recipe=Path(selected_area_settings.get("recipe_file")),
3069 mask=Path(selected_area_settings.get("mask_file")),
3070 ),
3071 )
3073 # make sure application file is valid for this pattern type:
3074 try:
3075 microscope.patterning.set_default_application_file(pattern.application)
3076 microscope.patterning.create_rectangle(
3077 center_x=0.0,
3078 center_y=0.0,
3079 width=10.0e-6,
3080 height=10.0e-6,
3081 depth=1.0e-6,
3082 )
3083 microscope.patterning.clear_patterns()
3084 except:
3085 raise ValueError(
3086 f'Invalid application file of "{pattern.application}" for Selected Area pattern. Please use an application file for Rectangle milling.'
3087 )
3089 return pattern
3090 else:
3091 raise KeyError(
3092 f"Invalid pattern type of {pattern_type}. Supported pattern types are: {[i.value for i in tbt.FIBPatternType]}"
3093 )
3096def validate_fib_box_settings(
3097 settings: dict,
3098 yml_format: tbt.YMLFormatVersion,
3099 step_name: str,
3100 pattern_type: tbt.FIBPatternType,
3101) -> bool:
3102 """
3103 Perform schema checking for FIB box pattern setting dictionary.
3105 This function validates the FIB box pattern settings dictionary based on the specified yml format.
3107 ## Parameters
3109 - `settings` (`dict`): The dictionary containing the FIB box pattern settings.
3110 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
3111 - `step_name` (`str`): The name of the step in the .yml file.
3112 - `pattern_type` (`tbt.FIBPatternType`): The type of the FIB pattern.
3114 ## Returns
3116 - `bool`: True if the settings are valid, False otherwise.
3118 ## Raises
3120 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
3121 """
3122 if yml_format.version >= 1.0: 3122 ↛ 3124, 3122 ↛ 31632 missed branches: 1) line 3122 didn't jump to line 3124 because the condition on line 3122 was never true, 2) line 3122 didn't jump to line 3163 because the condition on line 3122 was always true
3123 # flattens nested dictionary, adding "_" separator
3124 flat_settings = ut._flatten(settings)
3125 schema = Schema(
3126 {
3127 "center_x_um": And(
3128 float,
3129 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'x_um' parameter for pattern 'center' sub-dictionary must be a float. '{flat_settings['center_x_um']}' of type '{type(flat_settings['center_x_um'])}' was requested.",
3130 ),
3131 "center_y_um": And(
3132 float,
3133 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'y_um' parameter for pattern 'center' sub-dictionary must be a float. '{flat_settings['center_y_um']}' of type '{type(flat_settings['center_y_um'])}' was requested.",
3134 ),
3135 "width_um": And(
3136 float,
3137 lambda x: x > 0,
3138 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'width_um' parameter must be a float. '{flat_settings['width_um']}' of type '{type(flat_settings['width_um'])}' was requested.",
3139 ),
3140 "height_um": And(
3141 float,
3142 lambda x: x > 0,
3143 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'height_um' parameter must be a float. '{flat_settings['height_um']}' of type '{type(flat_settings['height_um'])}' was requested.",
3144 ),
3145 "depth_um": And(
3146 float,
3147 lambda x: x > 0,
3148 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'depth_um' parameter must be a float. '{flat_settings['depth_um']}' of type '{type(flat_settings['depth_um'])}' was requested.",
3149 ),
3150 "scan_direction": And(
3151 str,
3152 lambda x: ut.valid_enum_entry(x, tbt.FIBPatternScanDirection),
3153 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'scan_direction' parameter must be a valid scan direction type. '{flat_settings['scan_direction']}' was requested but supported values are: {[i.value for i in tbt.FIBPatternScanDirection]}",
3154 ),
3155 "scan_type": And(
3156 str,
3157 lambda x: ut.valid_enum_entry(x, tbt.FIBPatternScanType),
3158 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'scan_type' parameter must be a valid scan type type. '{flat_settings['scan_type']}' was requested but supported values are: {[i.value for i in tbt.FIBPatternScanType]}",
3159 ),
3160 },
3161 ignore_extra_keys=True,
3162 )
3163 try:
3164 schema.validate(flat_settings)
3165 except UnboundLocalError:
3166 raise ValueError(
3167 f"Error. Unsupported yml version {yml_format.version} provided."
3168 )
3169 return True
3172def validate_fib_selected_area_settings(
3173 settings: dict,
3174 yml_format: tbt.YMLFormatVersion,
3175 step_name: str,
3176 pattern_type: tbt.FIBPatternType,
3177) -> bool:
3178 """
3179 Perform schema checking for FIB selected area pattern setting dictionary.
3181 This function validates the FIB selected area pattern settings dictionary based on the specified yml format.
3183 ## Parameters
3185 - `settings` (`dict`): The dictionary containing the FIB selected area pattern settings.
3186 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
3187 - `step_name` (`str`): The name of the step in the .yml file.
3188 - `pattern_type` (`tbt.FIBPatternType`): The type of the FIB pattern.
3190 ## Returns
3192 - `bool`: True if the settings are valid, False otherwise.
3194 ## Raises
3196 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema.
3197 """
3198 if yml_format.version >= 1.0:
3199 schema = Schema( 3199 ↛ exitline 3199 didn't jump to the function exit
3200 {
3201 "dwell_us": And(
3202 float,
3203 lambda x: x > 0,
3204 # check all float error versions of modulus
3205 Or(
3206 lambda x: math.isclose(
3207 x % Constants.stream_pattern_base_dwell_us,
3208 0,
3209 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5,
3210 ),
3211 lambda x: math.isclose(
3212 x % Constants.stream_pattern_base_dwell_us,
3213 Constants.stream_pattern_base_dwell_us,
3214 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5,
3215 ),
3216 lambda x: math.isclose(
3217 x % Constants.stream_pattern_base_dwell_us,
3218 -Constants.stream_pattern_base_dwell_us,
3219 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5,
3220 ),
3221 ),
3222 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'dwell_us' parameter must be a positive float and an integer multiple of the base dwell time, {Constants.stream_pattern_base_dwell_us * Conversions.US_TO_NS} ns. '{settings['dwell_us']}' us of type '{type(settings['dwell_us'])}' was requested.",
3223 ),
3224 "repeats": And(
3225 int,
3226 lambda x: x > 0,
3227 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'repeats' parameter must be a positive integer. '{settings['repeats']}' of type '{type(settings['repeats'])}' was requested.",
3228 ),
3229 "recipe_file": And(
3230 str,
3231 lambda x: Path(x).is_file(),
3232 lambda x: Path(x).suffix == ".py",
3233 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'recipe_file' parameter must be a valid file path string with the extension '.py' and must already exist. The recipe file '{settings['recipe_file']}' was requested.",
3234 ),
3235 "mask_file": And(
3236 str,
3237 lambda x: Path(x).suffix == ".tif",
3238 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'mask' parameter must be a file path string with a file extension of '.tif'. '{settings['mask_file']}' of type '{type(settings['mask_file'])}' was requested.",
3239 ),
3240 },
3241 ignore_extra_keys=True,
3242 )
3243 try:
3244 schema.validate(settings)
3245 except UnboundLocalError:
3246 raise ValueError(
3247 f"Error. Unsupported yml version {yml_format.version} provided."
3248 )
3249 return True
3252def step(
3253 microscope: tbt.Microscope,
3254 # slice_number: str,
3255 step_name: str,
3256 step_settings: dict,
3257 general_settings: tbt.GeneralSettings,
3258 yml_format: tbt.YMLFormatVersion,
3259) -> tbt.Step:
3260 """
3261 Create a step object for different step types, including validation.
3263 This function creates a `Step` object for the specified step type and performs validation.
3265 ## Parameters
3267 - `microscope` (`tbt.Microscope`): The microscope object for which to create the step.
3268 - `step_name` (`str`): The name of the step in the .yml file.
3269 - `step_settings` (`dict`): The dictionary containing the step settings.
3270 - `general_settings` (`tbt.GeneralSettings`): The general settings object.
3271 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file.
3273 ## Returns
3275 - `tbt.Step`: The step object.
3277 ## Raises
3279 - `NotImplementedError`: If the step type is unsupported.
3280 - `KeyError`: If required settings are missing or invalid.
3281 """
3283 # parsing settings
3284 step_type_value = step_settings[yml_format.step_general_key][
3285 yml_format.step_type_key
3286 ]
3287 if not ut.valid_enum_entry(step_type_value, tbt.StepType):
3288 raise NotImplementedError(
3289 f"Unsupported step type of '{step_type_value}', for step name '{step_name}' supported types are: {[i.value for i in tbt.StepType]}."
3290 )
3291 step_type = tbt.StepType(step_type_value)
3293 step_number = step_settings[yml_format.step_general_key][yml_format.step_number_key]
3294 if not isinstance(step_number, int) or (step_number < 1):
3295 raise KeyError(
3296 f"Invalid step number of '{step_number}', for step name '{step_name}'. Must be a positive integer greater than 0."
3297 )
3298 step_frequency = step_settings[yml_format.step_general_key][
3299 yml_format.step_frequency_key
3300 ]
3301 if not isinstance(step_frequency, int) or (step_frequency < 1): 3301 ↛ 3305line 3301 didn't jump to line 3305 because the condition on line 3301 was always true
3302 raise KeyError(
3303 f"Invalid step frequency of '{step_frequency}', for step name '{step_name}'. Must be a positive integer greater than 0."
3304 )
3305 stage_db = step_settings[yml_format.step_general_key][
3306 yml_format.step_stage_settings_key
3307 ]
3309 # check and validate stage
3310 stage_settings = stage_position_settings(
3311 microscope=microscope,
3312 step_name=step_name,
3313 general_settings=general_settings,
3314 step_stage_settings=stage_db,
3315 yml_format=yml_format,
3316 )
3318 # TODO
3319 # operation_settings, could use match statement in python >= 3.10
3320 if step_type == tbt.StepType.EBSD:
3321 operation_settings = ebsd(
3322 microscope=microscope,
3323 step_settings=step_settings,
3324 step_name=step_name,
3325 yml_format=yml_format,
3326 )
3327 if step_type == tbt.StepType.EDS:
3328 operation_settings = eds(
3329 microscope=microscope,
3330 step_settings=step_settings,
3331 step_name=step_name,
3332 yml_format=yml_format,
3333 )
3334 if step_type == tbt.StepType.IMAGE:
3335 operation_settings = image(
3336 microscope=microscope,
3337 step_settings=step_settings,
3338 step_name=step_name,
3339 yml_format=yml_format,
3340 )
3341 if step_type == tbt.StepType.LASER:
3342 operation_settings = laser(
3343 microscope=microscope,
3344 step_settings=step_settings,
3345 step_name=step_name,
3346 yml_format=yml_format,
3347 )
3348 if step_type == tbt.StepType.CUSTOM:
3349 operation_settings = custom(
3350 microscope=microscope,
3351 step_settings=step_settings,
3352 step_name=step_name,
3353 yml_format=yml_format,
3354 )
3355 if step_type == tbt.StepType.FIB: 3355 ↛ 3363line 3355 didn't jump to line 3363 because the condition on line 3355 was always true
3356 operation_settings = fib(
3357 microscope=microscope,
3358 step_settings=step_settings,
3359 step_name=step_name,
3360 yml_format=yml_format,
3361 )
3363 step_object = tbt.Step(
3364 type=step_type,
3365 name=step_name,
3366 number=step_number,
3367 frequency=step_frequency,
3368 stage=stage_settings,
3369 operation_settings=operation_settings,
3370 )
3372 return step_object