Coverage for src/pytribeam/GUI/config_ui/lookup.py: 0%
381 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-06-16 18:30 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-06-16 18:30 +0000
1from typing import Any, Dict, NamedTuple, Optional, Type, Union
2from copy import deepcopy
3import tkinter as tk
4import pytribeam.GUI.CustomTkinterWidgets as ctk
6from pytribeam import types as tbt
7from pytribeam import utilities as ut
10VERSIONS = [version.version for version in tbt.YMLFormatVersion]
12# Import options from types
13beam_types = [i.value for i in tbt.BeamType]
14wavelengths = [i.value for i in tbt.LaserWavelength]
15polarizations = [i.value for i in tbt.LaserPolarization]
16coordinate_refs = [i.value for i in tbt.CoordinateReference]
17laser_scan_types_box = [i.value for i in tbt.LaserScanType]
18laser_scan_types_line = [i.value for i in tbt.LaserScanType]
19laser_pattern_modes = [i.value for i in tbt.LaserPatternMode]
20detector_types = [i.value for i in tbt.DetectorType]
21detector_modes = [i.value for i in tbt.DetectorMode]
22resolutions = [i.value for i in tbt.PresetResolution]
23fib_scan_dirs = [i.value for i in tbt.FIBPatternScanDirection]
24fib_scan_types = [i.value for i in tbt.FIBPatternScanType]
25bit_depths = [i.value for i in tbt.ColorDepth]
26rotation_sides = [i.value for i in tbt.RotationSide]
28# Options need empty values
29beam_types.append("")
30wavelengths.append("")
31coordinate_refs.append("")
32laser_scan_types_box.append("")
33laser_scan_types_line.append("")
34laser_pattern_modes.append("")
35detector_types.append("")
36detector_modes.append("")
37resolutions.append("WIDTHxHEIGHT")
38fib_scan_dirs.append("")
39fib_scan_types.append("")
40rotation_sides.append("")
43class LUTField(NamedTuple):
44 """Represents a single field in the LUT with both GUI and type information"""
46 label: str
47 default: Any
48 widget: Type[tk.Widget]
49 widget_kwargs: Dict[str, Any]
50 help_text: str
51 dtype: Type
52 version: tbt.Limit
55class LUT:
56 """Base class for type-aware lookup tables"""
58 def __init__(self, name: Optional[str] = None):
59 self._entries = {}
60 self.name = name
62 def __repr__(self):
63 return f"LUT({self.name})"
65 def __str__(self):
66 """Should behave like a dictionary."""
67 return str(self._entries)
69 def __getitem__(self, key: str) -> LUTField:
70 return self.get_entry(key)
72 def __setitem__(self, key: str, value: Union[LUTField, "LUT"]):
73 self.add_entry(key, value)
75 def __eq__(self, other: "LUT") -> bool:
76 """Compare two TypedLUTs for equality"""
77 if not isinstance(other, LUT):
78 return False
80 # Compare flattened versions to check structure and content
81 this = self._flatten()
82 that = other._flatten()
83 return this == that
85 def keys(self):
86 return self._entries.keys()
88 def values(self):
89 return self._entries.values()
91 def items(self):
92 return self._entries.items()
94 def add_entry(self, name: str, field: Union[LUTField, "LUT"]):
95 self._entries[name] = field
97 def get_entry(self, name: str) -> LUTField:
98 return self._entries[name]
100 def remove_entry(self, name: str):
101 return self._entries.pop(name)
103 def _prune_empty(self) -> bool:
104 """
105 Recursively remove empty nested LUTs.
106 Returns True if this node is empty after pruning.
107 """
108 to_delete = []
110 for name, entry in self._entries.items():
111 if isinstance(entry, LUT):
112 if entry._prune_empty():
113 to_delete.append(name)
115 for name in to_delete:
116 del self._entries[name]
118 return len(self._entries) == 0
120 def flatten(self, separator: str = "/") -> Dict[str, LUTField]:
121 """Flatten the LUT into a dictionary of fields with paths as keys."""
122 self._entries = self._flatten(separator=separator)
124 def _flatten(self, separator: str = "/", prefix: str = ""):
125 """Flattens the fields using a separator. This is done in place."""
126 flattened = {}
128 for name, entry in self._entries.items():
129 current_path = f"{prefix}{name}" if prefix else name
131 if isinstance(entry, LUTField):
132 flattened[current_path] = entry
133 elif isinstance(entry, LUT):
134 # Recursively flatten nested LUT
135 nested_flat = entry._flatten(
136 separator=separator, prefix=f"{current_path}{separator}"
137 )
138 flattened.update(nested_flat)
140 return flattened
142 def unflatten(self, separator: str = "/") -> "LUT":
143 """Reconstruct a TypedLUT from a flattened dictionary."""
144 self._entries = self._unflatten(self._entries, separator=separator)._entries
146 @classmethod
147 def _unflatten(cls, flat_dict: Dict[str, LUTField], separator: str = "/") -> "LUT":
148 """
149 Reconstruct a TypedLUT from a flattened dictionary.
150 Args:
151 flat_dict: Dictionary with path-based keys and LUTField values
152 separator: String used as path separator in the keys
154 Returns:
155 TypedLUT: Reconstructed hierarchical structure
156 """
157 root = cls()
159 for path, field in flat_dict.items():
160 # Split path into components
161 parts = path.split(separator)
163 # Start at root
164 current = root
166 # Create/traverse path
167 for i, part in enumerate(parts[:-1]): # All but last component
168 if part not in current._entries:
169 current._entries[part] = cls(name=part)
170 current = current._entries[part]
172 # Add the field at the final location
173 current._entries[parts[-1]] = field
175 return root
177 @property
178 def entries(self) -> Dict[str, LUTField]:
179 return self._entries
182### General LUT ###
183slice_thickness_um = LUTField(
184 "Slice Thickness (um)",
185 "",
186 ctk.Entry,
187 {"dtype": float},
188 "Thickness of the laser cut slice in micrometers.",
189 float,
190 tbt.Limit(min=1.0, max=max(VERSIONS)),
191)
192max_slice_num = LUTField(
193 "Max Slice Number",
194 "",
195 ctk.Entry,
196 {"dtype": int},
197 "The maximum slice number to cut. The experiment will stop after this slice number is complete.",
198 int,
199 tbt.Limit(min=1.0, max=max(VERSIONS)),
200)
201pre_tilt_deg = LUTField(
202 "Pre-Tilt Angle (deg)",
203 "",
204 ctk.Entry,
205 {"dtype": float},
206 "The angle to pre-tilt sample holder used. This angle impacts how stage movements are determined.",
207 float,
208 tbt.Limit(min=1.0, max=max(VERSIONS)),
209)
210sectioning_axis = LUTField(
211 "Sectioning Axis",
212 "Z",
213 ctk.MenuButton,
214 {"options": ["X", "Y", "Z"], "dtype": str, "state": "disabled"},
215 "The axis that the laser will cut along. Can be X, Y, or Z.",
216 str,
217 tbt.Limit(min=1.0, max=max(VERSIONS)),
218)
219stage_translational_tol_um = LUTField(
220 "Stage Translational Tolerance (um)",
221 0.5,
222 ctk.Entry,
223 {"dtype": float},
224 "The tolerance for translational stage movements in micrometers.",
225 float,
226 tbt.Limit(min=1.0, max=max(VERSIONS)),
227)
228stage_angular_tol_deg = LUTField(
229 "Stage Angular Tolerance (deg)",
230 0.02,
231 ctk.Entry,
232 {"dtype": float},
233 "The tolerance for angular stage movements in degrees.",
234 float,
235 tbt.Limit(min=1.0, max=max(VERSIONS)),
236)
237connection_host = LUTField(
238 "Connection Host",
239 "localhost",
240 ctk.Entry,
241 {"dtype": str},
242 "The host of the connection to the SEM.",
243 str,
244 tbt.Limit(min=1.0, max=max(VERSIONS)),
245)
246connection_port = LUTField(
247 "Connection Port",
248 "",
249 ctk.Entry,
250 {"dtype": int},
251 "The port of the connection to the SEM.",
252 int,
253 tbt.Limit(min=1.0, max=max(VERSIONS)),
254)
255ebsd_oem = LUTField(
256 "EBSD OEM",
257 "",
258 ctk.MenuButton,
259 {"options": ["EDAX", "Oxford", "null"], "dtype": str},
260 "The OEM of the EBSD system being used.",
261 str,
262 tbt.Limit(min=1.0, max=max(VERSIONS)),
263)
264eds_oem = LUTField(
265 "EDS OEM",
266 "",
267 ctk.MenuButton,
268 {"options": ["EDAX", "Oxford", "null"], "dtype": str},
269 "The OEM of the EDS system being used.",
270 str,
271 tbt.Limit(min=1.0, max=max(VERSIONS)),
272)
273exp_dir = LUTField(
274 "Experiment Directory",
275 "./",
276 ctk.PathEntry,
277 {"directory": True},
278 "The directory where the experiment data is saved.",
279 str,
280 tbt.Limit(min=1.0, max=max(VERSIONS)),
281)
282h5_log_name = LUTField(
283 "H5 Log Name",
284 "log",
285 ctk.Entry,
286 {"dtype": str},
287 "The name of the HDF5 log file.",
288 str,
289 tbt.Limit(min=1.0, max=max(VERSIONS)),
290)
291step_count = LUTField(
292 "Step Count",
293 0,
294 ctk.Entry,
295 {"dtype": int},
296 "The number of steps in the experiment.",
297 int,
298 tbt.Limit(min=1.0, max=max(VERSIONS)),
299)
300general_lut = LUT("general")
301general_lut.add_entry("slice_thickness_um", deepcopy(slice_thickness_um))
302general_lut.add_entry("max_slice_num", deepcopy(max_slice_num))
303general_lut.add_entry("pre_tilt_deg", deepcopy(pre_tilt_deg))
304general_lut.add_entry("sectioning_axis", deepcopy(sectioning_axis))
305general_lut.add_entry(
306 "stage_translational_tol_um", deepcopy(stage_translational_tol_um)
307)
308general_lut.add_entry("stage_angular_tol_deg", deepcopy(stage_angular_tol_deg))
309general_lut.add_entry("connection_host", deepcopy(connection_host))
310general_lut.add_entry("connection_port", deepcopy(connection_port))
311general_lut.add_entry("EBSD_OEM", deepcopy(ebsd_oem))
312general_lut.add_entry("EDS_OEM", deepcopy(eds_oem))
313general_lut.add_entry("exp_dir", deepcopy(exp_dir))
314general_lut.add_entry("h5_log_name", deepcopy(h5_log_name))
315general_lut.add_entry("step_count", deepcopy(step_count))
317### STAGE ###
318x_mm = LUTField(
319 "Start X Position (mm)",
320 "",
321 ctk.Entry,
322 {"dtype": float},
323 "The starting X position of the laser cut.",
324 float,
325 tbt.Limit(min=1.0, max=max(VERSIONS)),
326)
327y_mm = LUTField(
328 "Start Y Position (mm)",
329 "",
330 ctk.Entry,
331 {"dtype": float},
332 "The starting Y position of the laser cut.",
333 float,
334 tbt.Limit(min=1.0, max=max(VERSIONS)),
335)
336z_mm = LUTField(
337 "Start Z Position (mm)",
338 "",
339 ctk.Entry,
340 {"dtype": float},
341 "The starting Z position of the laser cut.",
342 float,
343 tbt.Limit(min=1.0, max=max(VERSIONS)),
344)
345t_deg = LUTField(
346 "Start T Position (°)",
347 "",
348 ctk.Entry,
349 {"dtype": float},
350 "The starting T position of the laser cut.",
351 float,
352 tbt.Limit(min=1.0, max=max(VERSIONS)),
353)
354r_deg = LUTField(
355 "Start R Position (°)",
356 "",
357 ctk.Entry,
358 {"dtype": float},
359 "The starting R position of the laser cut.",
360 float,
361 tbt.Limit(min=1.0, max=max(VERSIONS)),
362)
363rotation_side = LUTField(
364 "Rotation Side",
365 rotation_sides[-1],
366 ctk.MenuButton,
367 {"options": rotation_sides, "dtype": str},
368 "Whether the sample pretilt is in the laser position or the FIB position.",
369 str,
370 tbt.Limit(min=1.0, max=max(VERSIONS)),
371)
372initial_pos_lut = LUT("initial_position")
373initial_pos_lut.add_entry("x_mm", x_mm)
374initial_pos_lut.add_entry("y_mm", y_mm)
375initial_pos_lut.add_entry("z_mm", z_mm)
376initial_pos_lut.add_entry("t_deg", t_deg)
377initial_pos_lut.add_entry("r_deg", r_deg)
378stage_lut = LUT("stage")
379stage_lut.add_entry("rotation_side", rotation_side)
380stage_lut.add_entry("initial_position", initial_pos_lut)
382### COMMON ###
383step_name = LUTField(
384 "Step Name",
385 "",
386 ctk.Entry,
387 {"dtype": str},
388 "The name of the step.",
389 str,
390 tbt.Limit(min=1.0, max=max(VERSIONS)),
391)
392step_number = LUTField(
393 "Step Number",
394 "",
395 ctk.Entry,
396 {"state": "disabled", "dtype": int},
397 "The number of the step in the sequence of steps.",
398 int,
399 tbt.Limit(min=1.0, max=max(VERSIONS)),
400)
401step_type = LUTField(
402 "Step Type",
403 "",
404 ctk.Entry,
405 {"state": "disabled", "dtype": str},
406 "The step type.",
407 str,
408 tbt.Limit(min=1.0, max=max(VERSIONS)),
409)
410frequency = LUTField(
411 "Frequency",
412 1,
413 ctk.Entry,
414 {"dtype": int},
415 "The frequency that this step is activated (i.e. 1 means every slice, 2 means every other slice, etc.).",
416 int,
417 tbt.Limit(min=1.0, max=max(VERSIONS)),
418)
419common_lut = LUT("stage")
420common_lut.add_entry("step_name", step_name)
421common_lut.add_entry("step_number", step_number)
422common_lut.add_entry("step_type", step_type)
423common_lut.add_entry("frequency", frequency)
424common_lut.add_entry("stage", stage_lut)
426### AUTO_CB ###
427left = LUTField(
428 "Left Fraction",
429 "",
430 ctk.Entry,
431 {"dtype": float},
432 "Fractional position (of the entire image) for the left edge of the reduced area for auto contrast and brightness adjustment. Empty/None/null for all fractions turns off ACB.",
433 float,
434 tbt.Limit(min=1.0, max=max(VERSIONS)),
435)
436width = LUTField(
437 "Width Fraction",
438 "",
439 ctk.Entry,
440 {"dtype": float},
441 "The fractional width (of the entire image) to use for auto contrast and brightness. Empty/None for all fractions turns off ACB.",
442 float,
443 tbt.Limit(min=1.0, max=max(VERSIONS)),
444)
445top = LUTField(
446 "Top Fraction",
447 "",
448 ctk.Entry,
449 {"dtype": float},
450 "Fractional position (of the entire image) for the top edge of the reduced area for auto contrast and brightness adjustment. Empty/None for all fractions turns off ACB.",
451 float,
452 tbt.Limit(min=1.0, max=max(VERSIONS)),
453)
454height = LUTField(
455 "Height Fraction",
456 "",
457 ctk.Entry,
458 {"dtype": float},
459 "The fractional height (of the entire image) to use for auto contrast and brightness. Empty/None for all fractions turns off ACB.",
460 float,
461 tbt.Limit(min=1.0, max=max(VERSIONS)),
462)
463auto_cb_lut = LUT("auto_cb")
464auto_cb_lut.add_entry("left", left)
465auto_cb_lut.add_entry("width", width)
466auto_cb_lut.add_entry("top", top)
467auto_cb_lut.add_entry("height", height)
469### BEAM ###
470beam_type = LUTField(
471 "Beam Type",
472 beam_types[-1],
473 ctk.MenuButton,
474 {"options": beam_types, "dtype": str},
475 "The type of beam used to acquire the image.",
476 str,
477 tbt.Limit(min=1.0, max=max(VERSIONS)),
478)
479voltage_kv = LUTField(
480 "Beam Voltage (kV)",
481 "",
482 ctk.Entry,
483 {"dtype": float},
484 "The voltage of the beam in keV.",
485 float,
486 tbt.Limit(min=1.0, max=max(VERSIONS)),
487)
488voltage_tol_kv = LUTField(
489 "Beam Voltage Tolerance (kV)",
490 0.1,
491 ctk.Entry,
492 {"dtype": float},
493 "The tolerance of the beam voltage in kV.",
494 float,
495 tbt.Limit(min=1.0, max=max(VERSIONS)),
496)
497current_na = LUTField(
498 "Beam Current (nA)",
499 "",
500 ctk.Entry,
501 {"dtype": float},
502 "The current of the beam in nA.",
503 float,
504 tbt.Limit(min=1.0, max=max(VERSIONS)),
505)
506current_tol_na = LUTField(
507 "Beam Current Tolerance (nA)",
508 0.5,
509 ctk.Entry,
510 {"dtype": float},
511 "The tolerance of the beam current in nA.",
512 float,
513 tbt.Limit(min=1.0, max=max(VERSIONS)),
514)
515hfw_mm = LUTField(
516 "Horizontal Field Width (mm)",
517 "",
518 ctk.Entry,
519 {"dtype": float},
520 "The horizontal field width of the image in mm.",
521 float,
522 tbt.Limit(min=1.0, max=max(VERSIONS)),
523)
524working_dist_mm = LUTField(
525 "Working Distance (mm)",
526 "",
527 ctk.Entry,
528 {"dtype": float},
529 "The working distance of the image in mm.",
530 float,
531 tbt.Limit(min=1.0, max=max(VERSIONS)),
532)
533dynamic_focus = LUTField(
534 "Use Dynamic Focus",
535 False,
536 ctk.Checkbutton,
537 {"offvalue": False, "onvalue": True, "bd": 0, "dtype": bool},
538 "Whether to use dynamic focusing.",
539 bool,
540 tbt.Limit(min=1.0, max=max(VERSIONS)),
541)
542tilt_correction = LUTField(
543 "Use Tilt Correction",
544 False,
545 ctk.Checkbutton,
546 {"offvalue": False, "onvalue": True, "bd": 0, "dtype": bool},
547 "Whether to use tilt correction.",
548 bool,
549 tbt.Limit(min=1.0, max=max(VERSIONS)),
550)
551beam_lut = LUT("beam")
552beam_lut.add_entry("type", beam_type)
553beam_lut.add_entry("voltage_kv", voltage_kv)
554beam_lut.add_entry("voltage_tol_kv", voltage_tol_kv)
555beam_lut.add_entry("current_na", current_na)
556beam_lut.add_entry("current_tol_na", current_tol_na)
557beam_lut.add_entry("hfw_mm", hfw_mm)
558beam_lut.add_entry("working_dist_mm", working_dist_mm)
559beam_lut.add_entry("dynamic_focus", dynamic_focus)
560beam_lut.add_entry("tilt_correction", tilt_correction)
562### LASER ###
563laser_pulse_wavelength_nm = LUTField(
564 "Wavelength (nm)",
565 wavelengths[-1],
566 ctk.MenuButton,
567 {"options": wavelengths, "dtype": int},
568 "The wavelength of the laser.",
569 int,
570 tbt.Limit(min=1.0, max=max(VERSIONS)),
571)
572laser_pulse_divider = LUTField(
573 "Pulse Divider",
574 "",
575 ctk.Entry,
576 {"dtype": int},
577 "Determines the repetition rate of the laser.",
578 int,
579 tbt.Limit(min=1.0, max=max(VERSIONS)),
580)
581laser_pulse_energy_uj = LUTField(
582 "Energy (uJ)",
583 "",
584 ctk.Entry,
585 {"dtype": float},
586 "The energy of the laser pulse in microjoules.",
587 float,
588 tbt.Limit(min=1.0, max=max(VERSIONS)),
589)
590laser_pulse_polarization = LUTField(
591 "Polarization",
592 polarizations[0],
593 ctk.MenuButton,
594 {"options": polarizations, "dtype": str},
595 "The polarization of the laser.",
596 str,
597 tbt.Limit(min=1.0, max=max(VERSIONS)),
598)
599laser_objective_position_mm = LUTField(
600 "Objective Position (mm)",
601 "",
602 ctk.Entry,
603 {"dtype": float},
604 "The position of the objective lens in millimeters.",
605 float,
606 tbt.Limit(min=1.0, max=max(VERSIONS)),
607)
608laser_pattern_passes = LUTField(
609 "Passes",
610 "",
611 ctk.Entry,
612 {"dtype": int},
613 "The number of passes the laser will make.",
614 int,
615 tbt.Limit(min=1.0, max=max(VERSIONS)),
616)
617laser_pattern_size_x_um = LUTField(
618 "Size X (um)",
619 "",
620 ctk.Entry,
621 {"dtype": float},
622 "The size of the box in the X direction in micrometers.",
623 float,
624 tbt.Limit(min=1.0, max=max(VERSIONS)),
625)
626laser_pattern_size_y_um = LUTField(
627 "Size Y (um)",
628 "",
629 ctk.Entry,
630 {"dtype": float},
631 "The size of the box in the Y direction in micrometers.",
632 float,
633 tbt.Limit(min=1.0, max=max(VERSIONS)),
634)
635laser_pattern_pitch_x_um = LUTField(
636 "Pitch X (um)",
637 "",
638 ctk.Entry,
639 {"dtype": float},
640 "The pitch of the box in the X direction in micrometers.",
641 float,
642 tbt.Limit(min=1.0, max=max(VERSIONS)),
643)
644laser_pattern_pitch_y_um = LUTField(
645 "Pitch Y (um)",
646 "",
647 ctk.Entry,
648 {"dtype": float},
649 "The pitch of the box in the Y direction in micrometers.",
650 float,
651 tbt.Limit(min=1.0, max=max(VERSIONS)),
652)
653laser_pattern_scan_type = LUTField(
654 "Scan Type",
655 laser_scan_types_box[-1],
656 ctk.MenuButton,
657 {"options": laser_scan_types_box, "dtype": str},
658 "The type of scan to perform.",
659 str,
660 tbt.Limit(min=1.0, max=max(VERSIONS)),
661)
662laser_pattern_coordinate_ref = LUTField(
663 "Coordinate Reference",
664 coordinate_refs[-1],
665 ctk.MenuButton,
666 {"options": coordinate_refs, "dtype": str},
667 "The reference coordinate for the scan.",
668 str,
669 tbt.Limit(min=1.0, max=max(VERSIONS)),
670)
671laser_pattern_size_um = LUTField(
672 "Size (um)",
673 "",
674 ctk.Entry,
675 {"dtype": float},
676 "The size of the line in micrometers.",
677 float,
678 tbt.Limit(min=1.0, max=max(VERSIONS)),
679)
680laser_pattern_pitch_um = LUTField(
681 "Pitch (um)",
682 "",
683 ctk.Entry,
684 {"dtype": float},
685 "The pitch of the line in micrometers.",
686 float,
687 tbt.Limit(min=1.0, max=max(VERSIONS)),
688)
689laser_pattern_rotation_deg = LUTField(
690 "Scan Rotation (deg)",
691 0.0,
692 ctk.Entry,
693 {"dtype": float},
694 "The rotation of the scan in degrees.",
695 float,
696 tbt.Limit(min=1.0, max=max(VERSIONS)),
697)
698laser_pattern_mode = LUTField(
699 "Mode",
700 laser_pattern_modes[-1],
701 ctk.MenuButton,
702 {"options": laser_pattern_modes, "dtype": str},
703 "The mode of the laser.",
704 str,
705 tbt.Limit(min=1.0, max=max(VERSIONS)),
706)
707laser_pattern_pulses_per_pixel = LUTField(
708 "Pulses Per Pixel",
709 "",
710 ctk.Entry,
711 {"dtype": int},
712 "The number of pulses per pixel (only matters for fine).",
713 int,
714 tbt.Limit(min=1.0, max=max(VERSIONS)),
715)
716laser_pattern_pixel_dwell_ms = LUTField(
717 "Pixel Dwell Time (ms)",
718 "",
719 ctk.Entry,
720 {"dtype": float},
721 "The dwell time of the laser in milliseconds (only matters for coarse).",
722 float,
723 tbt.Limit(min=1.0, max=max(VERSIONS)),
724)
725laser_beam_shift_x_um = LUTField(
726 "Beam Shift X (um)",
727 0.0,
728 ctk.Entry,
729 {"dtype": float},
730 "The beam shift in the X direction in micrometers. Is applied on top of the hardware shift (i.e. it is applied in addition to any 'Beam Centering' values).",
731 float,
732 tbt.Limit(min=1.0, max=max(VERSIONS)),
733)
734laser_beam_shift_y_um = LUTField(
735 "Beam Shift Y (um)",
736 0.0,
737 ctk.Entry,
738 {"dtype": float},
739 "The beam shift in the Y direction in micrometers. Is applied on top of the hardware shift (i.e. it is applied in addition to any 'Beam Centering' values).",
740 float,
741 tbt.Limit(min=1.0, max=max(VERSIONS)),
742)
743laser_pulse_lut = LUT("pulse")
744laser_pulse_lut.add_entry("wavelength_nm", laser_pulse_wavelength_nm)
745laser_pulse_lut.add_entry("divider", laser_pulse_divider)
746laser_pulse_lut.add_entry("energy_uj", laser_pulse_energy_uj)
747laser_pulse_lut.add_entry("polarization", laser_pulse_polarization)
748laser_line_pattern_lut = LUT("line")
749laser_line_pattern_lut.add_entry("passes", laser_pattern_passes)
750laser_line_pattern_lut.add_entry("size_um", laser_pattern_size_um)
751laser_line_pattern_lut.add_entry("pitch_um", laser_pattern_pitch_um)
752laser_line_pattern_lut.add_entry("scan_type", laser_pattern_scan_type)
753laser_box_pattern_lut = LUT("box")
754laser_box_pattern_lut.add_entry("passes", laser_pattern_passes)
755laser_box_pattern_lut.add_entry("size_x_um", laser_pattern_size_x_um)
756laser_box_pattern_lut.add_entry("size_y_um", laser_pattern_size_y_um)
757laser_box_pattern_lut.add_entry("pitch_x_um", laser_pattern_pitch_x_um)
758laser_box_pattern_lut.add_entry("pitch_y_um", laser_pattern_pitch_y_um)
759laser_box_pattern_lut.add_entry("scan_type", laser_pattern_scan_type)
760laser_box_pattern_lut.add_entry("coordinate_ref", laser_pattern_coordinate_ref)
761laser_pattern_type_lut = LUT("type")
762laser_pattern_type_lut.add_entry("box", laser_box_pattern_lut)
763laser_pattern_type_lut.add_entry("line", laser_line_pattern_lut)
764laser_pattern_lut = LUT("pattern")
765laser_pattern_lut.add_entry("type", laser_pattern_type_lut)
766laser_pattern_lut.add_entry("rotation_deg", laser_pattern_rotation_deg)
767laser_pattern_lut.add_entry("mode", laser_pattern_mode)
768laser_pattern_lut.add_entry("pulses_per_pixel", laser_pattern_pulses_per_pixel)
769laser_pattern_lut.add_entry("pixel_dwell_ms", laser_pattern_pixel_dwell_ms)
770laser_beam_shift_lut = LUT("beam_shift")
771laser_beam_shift_lut.add_entry("x_um", laser_beam_shift_x_um)
772laser_beam_shift_lut.add_entry("y_um", laser_beam_shift_y_um)
773laser_lut = LUT("laser")
774laser_lut.add_entry("step_general", deepcopy(common_lut))
775laser_lut.add_entry("pulse", deepcopy(laser_pulse_lut))
776laser_lut.add_entry("objective_position_mm", deepcopy(laser_objective_position_mm))
777laser_lut.add_entry("beam_shift", deepcopy(laser_beam_shift_lut))
778laser_lut.add_entry("pattern", deepcopy(laser_pattern_lut))
779# Laser should have a step type and name of laser
781### IMAGE ###
782detector_type = LUTField(
783 "Detector Type",
784 detector_types[-1],
785 ctk.MenuButton,
786 {"options": detector_types, "dtype": str},
787 "The type of detector used to acquire the image.",
788 str,
789 tbt.Limit(min=1.0, max=max(VERSIONS)),
790)
791detector_mode = LUTField(
792 "Detector Mode",
793 detector_modes[-1],
794 ctk.MenuButton,
795 {"options": detector_modes, "dtype": str},
796 "The mode of the detector used to acquire the image.",
797 str,
798 tbt.Limit(min=1.0, max=max(VERSIONS)),
799)
800brightness_fraction = LUTField(
801 "Brightness Fraction",
802 "",
803 ctk.Entry,
804 {"dtype": float},
805 "(If auto contrast/brightness is False) The fractional brightness value to use.",
806 float,
807 tbt.Limit(min=1.0, max=max(VERSIONS)),
808)
809contrast_fraction = LUTField(
810 "Contrast Fraction",
811 "",
812 ctk.Entry,
813 {"dtype": float},
814 "(If auto contrast/brightness is False) The fractional contrast value to use.",
815 float,
816 tbt.Limit(min=1.0, max=max(VERSIONS)),
817)
818image_rotation_deg = LUTField(
819 "Scan Rotation (deg)",
820 0.0,
821 ctk.Entry,
822 {"dtype": float},
823 "The rotation of the scan in degrees.",
824 float,
825 tbt.Limit(min=1.0, max=max(VERSIONS)),
826)
827image_dwell_time_us = LUTField(
828 "Dwell Time (us)",
829 "",
830 ctk.Entry,
831 {"dtype": float},
832 "The dwell time of the image in microseconds.",
833 float,
834 tbt.Limit(min=1.0, max=max(VERSIONS)),
835)
836image_resolution = LUTField(
837 "Resolution",
838 resolutions[-1],
839 ctk.EntryMenuButton,
840 {"options": resolutions, "dtype": str},
841 "The resolution of the image. Can be a present or custom resolution.",
842 str,
843 tbt.Limit(min=1.0, max=max(VERSIONS)),
844)
845image_bit_depth = LUTField(
846 "Bit Depth",
847 bit_depths[0],
848 ctk.MenuButton,
849 {"options": bit_depths, "dtype": int},
850 "The bit depth of the image.",
851 int,
852 tbt.Limit(min=1.0, max=max(VERSIONS)),
853)
854detector_lut = LUT("detector")
855detector_lut.add_entry("type", detector_type)
856detector_lut.add_entry("mode", detector_mode)
857detector_lut.add_entry("brightness", brightness_fraction)
858detector_lut.add_entry("contrast", contrast_fraction)
859detector_lut.add_entry("auto_cb", auto_cb_lut)
860scan_lut = LUT("scan")
861scan_lut.add_entry("rotation_deg", image_rotation_deg)
862scan_lut.add_entry("dwell_time_us", image_dwell_time_us)
863scan_lut.add_entry("resolution", image_resolution)
864image_lut = LUT("image")
865image_lut.add_entry("step_general", deepcopy(common_lut))
866image_lut.add_entry("beam", deepcopy(beam_lut))
867image_lut.add_entry("detector", deepcopy(detector_lut))
868image_lut.add_entry("scan", deepcopy(scan_lut))
869image_lut.add_entry("bit_depth", deepcopy(image_bit_depth))
870# Image should have a step type and name of image
872### EDS ###
873eds_lut = LUT("eds")
874eds_lut.add_entry("step_general", deepcopy(common_lut))
875eds_lut.add_entry("beam", deepcopy(beam_lut))
876eds_lut.add_entry("detector", deepcopy(detector_lut))
877eds_lut.add_entry("scan", deepcopy(scan_lut))
878eds_lut.add_entry("bit_depth", deepcopy(image_bit_depth))
879# EDS should be electron beam types only, and should have a step type and name of eds
881### EBSD ###
882ebsd_concurrent_eds = LUTField(
883 "Concurrent EDS",
884 False,
885 ctk.Checkbutton,
886 {"offvalue": False, "onvalue": True, "bd": 0, "dtype": bool},
887 "Whether to acquire EDS data concurrently with the EBSD data.",
888 bool,
889 tbt.Limit(min=1.0, max=max(VERSIONS)),
890)
891ebsd_lut = LUT("ebsd")
892ebsd_lut.add_entry("step_general", deepcopy(common_lut))
893ebsd_lut.add_entry("beam", deepcopy(beam_lut))
894ebsd_lut.add_entry("detector", deepcopy(detector_lut))
895ebsd_lut.add_entry("scan", deepcopy(scan_lut))
896ebsd_lut.add_entry("bit_depth", deepcopy(image_bit_depth))
897ebsd_lut.add_entry("concurrent_EDS", deepcopy(ebsd_concurrent_eds))
898# EBSD should be electron beam types only, and should have a step type and name of ebsd
900### PFIB ###
901center_x_um = LUTField(
902 "Center X (um)",
903 "",
904 ctk.Entry,
905 {"dtype": float},
906 "The X coordinate of the center of the milling pattern.",
907 float,
908 tbt.Limit(min=1.0, max=max(VERSIONS)),
909)
910center_y_um = LUTField(
911 "Center Y (um)",
912 "",
913 ctk.Entry,
914 {"dtype": float},
915 "The Y coordinate of the center of the milling pattern.",
916 float,
917 tbt.Limit(min=1.0, max=max(VERSIONS)),
918)
919width_um = LUTField(
920 "Width (um)",
921 "",
922 ctk.Entry,
923 {"dtype": float},
924 "The width of the rectangle in micrometers.",
925 float,
926 tbt.Limit(min=1.0, max=max(VERSIONS)),
927)
928height_um = LUTField(
929 "Height (um)",
930 "",
931 ctk.Entry,
932 {"dtype": float},
933 "The height of the rectangle in micrometers.",
934 float,
935 tbt.Limit(min=1.0, max=max(VERSIONS)),
936)
937depth_um = LUTField(
938 "Depth (um)",
939 "",
940 ctk.Entry,
941 {"dtype": float},
942 "The depth of the rectangle in micrometers.",
943 float,
944 tbt.Limit(min=1.0, max=max(VERSIONS)),
945)
946scan_direction = LUTField(
947 "Scan Direction",
948 fib_scan_dirs[-1],
949 ctk.MenuButton,
950 {"options": fib_scan_dirs, "dtype": str},
951 "The direction of the scan.",
952 str,
953 tbt.Limit(min=1.0, max=max(VERSIONS)),
954)
955scan_type = LUTField(
956 "Scan Type",
957 fib_scan_types[-1],
958 ctk.MenuButton,
959 {"options": fib_scan_types, "dtype": str},
960 "The type of scan to perform.",
961 str,
962 tbt.Limit(min=1.0, max=max(VERSIONS)),
963)
964dwell_us = LUTField(
965 "Mill Dwell Time (us)",
966 "",
967 ctk.Entry,
968 {"dtype": float},
969 "The dwell time of the mill in microseconds.",
970 float,
971 tbt.Limit(min=1.0, max=max(VERSIONS)),
972)
973repeats = LUTField(
974 "Pattern Repeats",
975 "",
976 ctk.Entry,
977 {"dtype": int},
978 "The number of times to repeat the pattern.",
979 int,
980 tbt.Limit(min=1.0, max=max(VERSIONS)),
981)
982recipe_file = LUTField(
983 "Image Processing Recipe",
984 "",
985 ctk.PathEntry,
986 {"directory": False, "defaultextension": ".py"},
987 "The recipe to use for image processing. Must be a python (.py) file.",
988 str,
989 tbt.Limit(min=1.0, max=max(VERSIONS)),
990)
991mask_file = LUTField(
992 "Mask File",
993 "",
994 ctk.PathEntry,
995 {"directory": False, "defaultextension": ".tif"},
996 "During this step, the mask file to use for milling will be saved (and overwritten) in this location. Should be a tiff (.tif) file. All masks will be saved automatically during the experiment.",
997 str,
998 tbt.Limit(min=1.0, max=max(VERSIONS)),
999)
1000application_file = LUTField(
1001 "Mill Pattern Preset",
1002 "",
1003 ctk.Entry,
1004 {"dtype": str},
1005 "The preset to use for milling.",
1006 str,
1007 tbt.Limit(min=1.0, max=max(VERSIONS)),
1008)
1009fib_center_lut = LUT("center")
1010fib_center_lut.add_entry("x_um", center_x_um)
1011fib_center_lut.add_entry("y_um", center_y_um)
1012fib_rectangle_pattern_lut = LUT("rectangle")
1013fib_rectangle_pattern_lut.add_entry("center", fib_center_lut)
1014fib_rectangle_pattern_lut.add_entry("width_um", width_um)
1015fib_rectangle_pattern_lut.add_entry("height_um", height_um)
1016fib_rectangle_pattern_lut.add_entry("depth_um", depth_um)
1017fib_rectangle_pattern_lut.add_entry("scan_direction", scan_direction)
1018fib_rectangle_pattern_lut.add_entry("scan_type", scan_type)
1019fib_selected_pattern_lut = LUT("selected_area")
1020fib_selected_pattern_lut.add_entry("dwell_us", dwell_us)
1021fib_selected_pattern_lut.add_entry("repeats", repeats)
1022fib_selected_pattern_lut.add_entry("recipe_file", recipe_file)
1023fib_selected_pattern_lut.add_entry("mask_file", mask_file)
1024fib_pattern_type_lut = LUT("type")
1025fib_pattern_type_lut.add_entry("rectangle", fib_rectangle_pattern_lut)
1026fib_pattern_type_lut.add_entry("regular_cross_section", fib_rectangle_pattern_lut)
1027fib_pattern_type_lut.add_entry("cleaning_cross_section", fib_rectangle_pattern_lut)
1028fib_pattern_type_lut.add_entry("selected_area", fib_selected_pattern_lut)
1029fib_pattern_lut = LUT("pattern")
1030fib_pattern_lut.add_entry("application_file", application_file)
1031fib_pattern_lut.add_entry("type", fib_pattern_type_lut)
1032fib_mill_lut = LUT("mill")
1033fib_mill_lut.add_entry("beam", beam_lut)
1034fib_mill_lut.add_entry("pattern", fib_pattern_lut)
1035fib_image_lut = LUT("image")
1036fib_image_lut.add_entry("beam", beam_lut)
1037fib_image_lut.add_entry("detector", detector_lut)
1038fib_image_lut.add_entry("scan", scan_lut)
1039fib_image_lut.add_entry("bit_depth", image_bit_depth)
1040fib_lut = LUT("fib")
1041fib_lut.add_entry("step_general", deepcopy(common_lut))
1042fib_lut.add_entry("image", deepcopy(fib_image_lut))
1043fib_lut.add_entry("mill", deepcopy(fib_mill_lut))
1044# FIB should be ion beam types only, dynamic focus should be off, and tilt correction should be off, and step type and name should be fib
1046### Custom step ###
1047custom_executable_path = LUTField(
1048 "Executable Path",
1049 "",
1050 ctk.PathEntry,
1051 {"directory": False, "operation": "open"},
1052 "The path to the executable to run. For python, this would be the location of the python executable.",
1053 str,
1054 tbt.Limit(min=1.0, max=max(VERSIONS)),
1055)
1056custom_script_path = LUTField(
1057 "Custom Script Path",
1058 "",
1059 ctk.PathEntry,
1060 {"directory": False, "operation": "open"},
1061 "The path to the custom script to run.",
1062 str,
1063 tbt.Limit(min=1.0, max=max(VERSIONS)),
1064)
1065custom_lut = LUT("custom")
1066custom_lut.add_entry("step_general", deepcopy(common_lut))
1067custom_lut.add_entry("executable_path", deepcopy(custom_executable_path))
1068custom_lut.add_entry("script_path", deepcopy(custom_script_path))
1069# Custom should have a step type and name of custom
1071# Apply edits to luts based on step type (enforce names/types, disable/enable fields, etc.)
1072laser_lut["step_general"]["step_type"] = LUTField(
1073 "Step Type",
1074 "laser",
1075 ctk.Entry,
1076 {"state": "disabled", "dtype": str},
1077 "The step type.",
1078 str,
1079 tbt.Limit(min=1.0, max=max(VERSIONS)),
1080)
1081laser_lut["step_general"]["step_name"] = LUTField(
1082 "Step Name",
1083 "laser",
1084 ctk.Entry,
1085 {"dtype": str},
1086 "The name of the step.",
1087 str,
1088 tbt.Limit(min=1.0, max=max(VERSIONS)),
1089)
1090image_lut["step_general"]["step_type"] = LUTField(
1091 "Step Type",
1092 "image",
1093 ctk.Entry,
1094 {"state": "disabled", "dtype": str},
1095 "The step type.",
1096 str,
1097 tbt.Limit(min=1.0, max=max(VERSIONS)),
1098)
1099image_lut["step_general"]["step_name"] = LUTField(
1100 "Step Name",
1101 "image",
1102 ctk.Entry,
1103 {"dtype": str},
1104 "The name of the step.",
1105 str,
1106 tbt.Limit(min=1.0, max=max(VERSIONS)),
1107)
1108eds_lut["step_general"]["step_type"] = LUTField(
1109 "Step Type",
1110 "eds",
1111 ctk.Entry,
1112 {"state": "disabled", "dtype": str},
1113 "The step type.",
1114 str,
1115 tbt.Limit(min=1.0, max=max(VERSIONS)),
1116)
1117eds_lut["step_general"]["step_name"] = LUTField(
1118 "Step Name",
1119 "eds",
1120 ctk.Entry,
1121 {"dtype": str},
1122 "The name of the step.",
1123 str,
1124 tbt.Limit(min=1.0, max=max(VERSIONS)),
1125)
1126eds_lut["beam"]["type"] = LUTField(
1127 "Beam Type",
1128 beam_types[0],
1129 ctk.MenuButton,
1130 {"options": beam_types, "dtype": str, "state": "disabled"},
1131 "The type of beam used to acquire the image.",
1132 str,
1133 tbt.Limit(min=1.0, max=max(VERSIONS)),
1134)
1135ebsd_lut["step_general"]["step_type"] = LUTField(
1136 "Step Type",
1137 "ebsd",
1138 ctk.Entry,
1139 {"state": "disabled", "dtype": str},
1140 "The step type.",
1141 str,
1142 tbt.Limit(min=1.0, max=max(VERSIONS)),
1143)
1144ebsd_lut["step_general"]["step_name"] = LUTField(
1145 "Step Name",
1146 "ebsd",
1147 ctk.Entry,
1148 {"dtype": str},
1149 "The name of the step.",
1150 str,
1151 tbt.Limit(min=1.0, max=max(VERSIONS)),
1152)
1153ebsd_lut["beam"]["type"] = LUTField(
1154 "Beam Type",
1155 beam_types[0],
1156 ctk.MenuButton,
1157 {"options": beam_types, "dtype": str, "state": "disabled"},
1158 "The type of beam used to acquire the image.",
1159 str,
1160 tbt.Limit(min=1.0, max=max(VERSIONS)),
1161)
1162fib_lut["step_general"]["step_type"] = LUTField(
1163 "Step Type",
1164 "fib",
1165 ctk.Entry,
1166 {"state": "disabled", "dtype": str},
1167 "The step type.",
1168 str,
1169 tbt.Limit(min=1.0, max=max(VERSIONS)),
1170)
1171fib_lut["step_general"]["step_name"] = LUTField(
1172 "Step Name",
1173 "fib",
1174 ctk.Entry,
1175 {"dtype": str},
1176 "The name of the step.",
1177 str,
1178 tbt.Limit(min=1.0, max=max(VERSIONS)),
1179)
1180fib_lut["image"]["beam"]["type"] = LUTField(
1181 "Beam Type",
1182 beam_types[1],
1183 ctk.MenuButton,
1184 {"options": beam_types, "dtype": str, "state": "disabled"},
1185 "The type of beam used to acquire the image.",
1186 str,
1187 tbt.Limit(min=1.0, max=max(VERSIONS)),
1188)
1189fib_lut["image"]["beam"]["dynamic_focus"] = LUTField(
1190 "Use Dynamic Focus",
1191 False,
1192 ctk.Checkbutton,
1193 {
1194 "offvalue": False,
1195 "onvalue": True,
1196 "bd": 0,
1197 "dtype": bool,
1198 "state": "disabled",
1199 },
1200 "Whether to use dynamic focusing.",
1201 bool,
1202 tbt.Limit(min=1.0, max=max(VERSIONS)),
1203)
1204fib_lut["image"]["beam"]["tilt_correction"] = LUTField(
1205 "Use Tilt Correction",
1206 False,
1207 ctk.Checkbutton,
1208 {
1209 "offvalue": False,
1210 "onvalue": True,
1211 "bd": 0,
1212 "dtype": bool,
1213 "state": "disabled",
1214 },
1215 "Whether to use tilt correction.",
1216 bool,
1217 tbt.Limit(min=1.0, max=max(VERSIONS)),
1218)
1219fib_lut["mill"]["beam"]["type"] = LUTField(
1220 "Beam Type",
1221 beam_types[1],
1222 ctk.MenuButton,
1223 {"options": beam_types, "dtype": str, "state": "disabled"},
1224 "The type of beam used to acquire the image.",
1225 str,
1226 tbt.Limit(min=1.0, max=max(VERSIONS)),
1227)
1228fib_lut["mill"]["beam"]["dynamic_focus"] = LUTField(
1229 "Use Dynamic Focus",
1230 False,
1231 ctk.Checkbutton,
1232 {
1233 "offvalue": False,
1234 "onvalue": True,
1235 "bd": 0,
1236 "dtype": bool,
1237 "state": "disabled",
1238 },
1239 "Whether to use dynamic focusing.",
1240 bool,
1241 tbt.Limit(min=1.0, max=max(VERSIONS)),
1242)
1243fib_lut["mill"]["beam"]["tilt_correction"] = LUTField(
1244 "Use Tilt Correction",
1245 False,
1246 ctk.Checkbutton,
1247 {
1248 "offvalue": False,
1249 "onvalue": True,
1250 "bd": 0,
1251 "dtype": bool,
1252 "state": "disabled",
1253 },
1254 "Whether to use tilt correction.",
1255 bool,
1256 tbt.Limit(min=1.0, max=max(VERSIONS)),
1257)
1258custom_lut["step_general"]["step_type"] = LUTField(
1259 "Step Type",
1260 "custom",
1261 ctk.Entry,
1262 {"state": "disabled", "dtype": str},
1263 "The step type.",
1264 str,
1265 tbt.Limit(min=1.0, max=max(VERSIONS)),
1266)
1267custom_lut["step_general"]["step_name"] = LUTField(
1268 "Step Name",
1269 "custom",
1270 ctk.Entry,
1271 {"dtype": str},
1272 "The name of the step.",
1273 str,
1274 tbt.Limit(min=1.0, max=max(VERSIONS)),
1275)
1278LUTs = {
1279 "general": general_lut,
1280 "laser": laser_lut,
1281 "image": image_lut,
1282 "fib": fib_lut,
1283 "eds": eds_lut,
1284 "ebsd": ebsd_lut,
1285 "custom": custom_lut,
1286}
1289class VersionedLUT:
1290 def __init__(self):
1291 self.versions = VERSIONS
1292 self._default_version = max(VERSIONS)
1293 self.LUTs = deepcopy(LUTs)
1295 def get_lut(self, step_type: str, version: Optional[str] = None) -> LUT:
1296 """Retrieve the LUT for the given version and step type.
1297 This is done by walking through the LUTs and removing the ones that don't match the version and the step type.
1298 If the version is not provided, the highest version is used.
1299 """
1300 if version is None:
1301 version = self._default_version
1302 if not any(version == v for v in self.versions):
1303 raise ValueError(
1304 f"Version {version} is not in the list of versions: {self.versions}"
1305 )
1306 if step_type.lower() not in self.LUTs:
1307 raise ValueError(
1308 f"Step type {step_type} is not in the list of step types: {list(self.LUTs.keys())}"
1309 )
1311 lut = deepcopy(self.LUTs[step_type.lower()])
1312 lut.flatten()
1313 items = list(lut.entries.items())
1314 for name, entry in items:
1315 if not ut.in_interval(version, entry.version, tbt.IntervalType.CLOSED):
1316 lut.remove_entry(name)
1317 lut.unflatten()
1318 lut._prune_empty()
1319 return lut
1322def get_lut(step_type: str, version: Optional[str] = None) -> LUT:
1323 """Retrieve the LUT for the given version and step type.
1324 This is done by walking through the LUTs and removing the ones that don't match the version and the step type.
1325 If the version is not provided, the highest version is used.
1326 """
1327 VLUT = VersionedLUT()
1328 return VLUT.get_lut(step_type, version)
1331if __name__ == "__main__":
1332 out_1_1 = get_lut("general", 1.1)
1333 print(out_1_1.keys())
1334 out_1_1.flatten()
1335 print(out_1_1.keys())