Coverage for src/pytribeam/laser.py: 32%
247 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-09-09 18:27 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-09-09 18:27 +0000
1#!/usr/bin/python3
2"""Laser, EBSD, and EDS hardware-control utilities.
4This module provides utilities for configuring and operating the femtosecond
5laser system used by `pytribeam`. It includes helpers for checking Laser API
6connectivity, applying laser pulse and patterning settings, moving the laser
7objective, adjusting laser beam shift, controlling the laser shutter, executing
8laser patterning, and starting EBSD/EDS maps through the Laser API.
10Most workflow code should use `laser_operation` or `mill_region` rather than
11calling low-level hardware-control helpers directly. Lower-level functions are
12available for interactive use, GUI control, diagnostics, and specialized
13workflows.
15## External dependency
17Laser control is performed through Thermo Fisher's `Laser.PythonControl` API,
18imported as `tfs_laser`. The API must be installed and importable for laser,
19EBSD, and EDS operations to work.
21Use `laser_connected` to test whether the laser API can communicate with the
22laser:
24```python
25from pytribeam import laser
27if not laser.laser_connected():
28 raise RuntimeError("Laser is not connected.")
29```
31## Typical usage
33Run a laser milling operation from workflow settings:
35```python
36from pytribeam import laser
38laser.laser_operation(
39 step=step,
40 general_settings=general_settings,
41 slice_number=slice_number,
42)
43```
45Apply laser settings and mill a configured region directly:
47```python
48from pytribeam import laser
50laser.mill_region(settings=laser_settings)
51```
53Start EBSD or EDS mapping:
55```python
56from pytribeam import laser
58laser.map_ebsd()
59laser.map_eds()
60```
62## Main entry points
64- `laser_connected`: check whether the Laser API can communicate with the laser.
65- `laser_state_to_db`: flatten a `tbt.LaserState` for display or GUI use.
66- `apply_laser_settings`: apply pulse, objective, beam-shift, scan-rotation, and
67 pattern settings.
68- `mill_region`: configure the laser, execute patterning, and restore scan
69 rotation.
70- `laser_operation`: perform a full workflow laser operation, including pre- and
71 post-operation power logging.
72- `map_ebsd`: start an EBSD map and check that it ran for the expected minimum
73 duration.
74- `map_eds`: start an EDS map and check that it ran for the expected minimum
75 duration.
77## Laser configuration workflow
79`mill_region` performs the standard laser milling sequence:
811. Verify that the laser is connected.
822. Enable access to insertable devices.
833. Record the active imaging beam and scan rotation.
844. Apply the requested laser settings.
855. Insert the laser shutter.
866. Start laser patterning.
877. Retract the laser shutter.
888. Restore the original imaging scan rotation.
90`laser_operation` wraps this sequence with laser-power measurements before and
91after milling and records those values in the experiment log.
93## Pattern support
95`create_pattern` currently supports:
97| Geometry type | Laser API pattern |
98| --- | --- |
99| `tbt.LaserBoxPattern` | Box pattern |
100| `tbt.LaserLinePattern` | Line pattern |
102Unsupported laser pattern geometry types raise `ValueError`.
104## Units
106Laser settings use explicit field names to indicate units:
108| Quantity | Units |
109| --- | --- |
110| Wavelength | nanometers |
111| Frequency | kilohertz |
112| Pulse energy | microjoules |
113| Objective position | millimeters |
114| Beam shift | micrometers |
115| Pattern size and pitch | micrometers |
116| Pixel dwell | milliseconds |
117| Pattern rotation | degrees |
118| Laser power | watts |
120## EBSD and EDS mapping
122EBSD and EDS mapping are started through the Laser API. The mapping functions
123check that the operation takes at least `Constants.min_map_time_s`; shorter
124durations are treated as likely mapping-software failures.
126> **Warning**
127>
128> Functions in this module can move hardware, fire the laser, insert or retract
129> the laser shutter, and start EBSD/EDS acquisition. Confirm that the microscope,
130> stage, sample, detectors, laser objective, shutter state, and beam-line
131> conditions are safe before calling these functions.
133<hr style="height: 12px; background-color: #333; border: none;">
134"""
136__all__ = [
137 "laser_state_to_db",
138 "laser_connected",
139 "pattern_mode",
140 "pulse_energy_uj",
141 "pulse_divider",
142 "set_wavelength",
143 "read_power",
144 "insert_shutter",
145 "retract_shutter",
146 "pulse_polarization",
147 "pulse_settings",
148 "retract_laser_objective",
149 "objective_position",
150 "beam_shift",
151 "create_pattern",
152 "apply_laser_settings",
153 "execute_patterning",
154 "mill_region",
155 "laser_operation",
156 "map_ebsd",
157 "map_eds",
158]
160# Default python modules
161import time
162import contextlib, io
163import math
165try:
166 import Laser.PythonControl as tfs_laser
168 print("Laser PythonControl API imported.")
169except:
170 print("WARNING: Laser API not imported!")
171 print("\tLaser control, as well as EBSD and EDS control are unavailable.")
173# 3rd party .whl modules
175# Local scripts
176from pytribeam.constants import Constants
177import pytribeam.factory as factory
178import pytribeam.types as tbt
179import pytribeam.utilities as ut
180import pytribeam.insertable_devices as devices
181import pytribeam.image as img
182import pytribeam.log as log
185def laser_state_to_db(state: tbt.LaserState) -> dict:
186 """
187 This function converts a `LaserState` object into a flattened dictionary representation.
189 ## Parameters
191 - `state` (`tbt.LaserState`): The laser state object to convert.
193 ## Returns
195 - `dict`: A flattened dictionary representation of the laser state.
196 """
197 db = {}
199 db["wavelength_nm"] = state.wavelength_nm
200 db["frequency_khz"] = state.frequency_khz
201 db["pulse_divider"] = state.pulse_divider
202 db["pulse_energy_uj"] = state.pulse_energy_uj
203 db["objective_position_mm"] = state.objective_position_mm
204 db["expected_pattern_duration_s"] = state.expected_pattern_duration_s
206 beam_shift = state.beam_shift_um
207 db["beam_shift_um_x"] = beam_shift.x
208 db["beam_shift_um_y"] = beam_shift.y
210 # we can name these differently depending on the needs of the GUI
211 pattern = state.pattern
212 db["laser_pattern_mode"] = pattern.mode.value
213 db["laser_pattern_rotation_deg"] = pattern.rotation_deg
214 db["laser_pattern_pulses_per_pixel"] = pattern.pulses_per_pixel
215 db["laser_pattern_pixel_dwell_ms"] = pattern.pixel_dwell_ms
217 geometry = pattern.geometry
218 db["passes"] = geometry.passes
219 db["laser_scan_type"] = geometry.scan_type.value
220 db["geometry_type"] = geometry.type.value
222 if geometry.type == tbt.LaserPatternType.BOX:
223 db["size_x_um"] = geometry.size_x_um
224 db["size_y_um"] = geometry.size_y_um
225 db["pitch_x_um"] = geometry.pitch_x_um
226 db["pitch_y_um"] = geometry.pitch_y_um
227 db["coordinate_ref"] = geometry.coordinate_ref
229 if geometry.type == tbt.LaserPatternType.LINE: 229 ↛ 233line 229 didn't jump to line 233 because the condition on line 229 was always true
230 db["size_um"] = geometry.size_um
231 db["pitch_um"] = geometry.pitch_um
233 return db
236def laser_connected() -> bool:
237 """
238 Check if the laser is connected.
240 This function tests the connection to the laser and returns True if the connection is successful.
242 ## Returns
244 bool
245 True if the laser is connected, False otherwise.
247 """
248 connect_msg = "Connection test successful.\n"
249 laser_status = io.StringIO()
250 try:
251 with contextlib.redirect_stdout(laser_status):
252 tfs_laser.TestConnection()
253 except:
254 return False
255 else:
256 if laser_status.getvalue() == connect_msg:
257 return True
258 return False
261def _device_connections() -> tbt.DeviceStatus:
262 """
263 Check the connection status of the laser and associated external devices.
265 This function checks the connection status of the laser, EBSD, and EDS devices. It is meant to be a quick tool for the GUI and does not provide additional information for troubleshooting.
267 ## Returns
269 tbt.DeviceStatus
270 The connection status of the laser, EBSD, and EDS devices.
272 """
273 # laser must be connected to connect with other devices:
274 if not laser_connected():
275 laser = tbt.RetractableDeviceState.ERROR
276 ebsd = tbt.RetractableDeviceState.ERROR
277 eds = tbt.RetractableDeviceState.ERROR
278 else:
279 laser = tbt.RetractableDeviceState.CONNECTED
280 ebsd = devices.connect_EBSD() # retractable device state
281 eds = devices.connect_EDS() # retractable device state
283 return tbt.DeviceStatus(
284 laser=laser,
285 ebsd=ebsd,
286 eds=eds,
287 )
290def pattern_mode(mode: tbt.LaserPatternMode) -> bool:
291 """
292 Set the laser pattern mode.
294 This function sets the laser pattern mode and verifies that it has been set correctly.
296 ## Parameters
298 - `mode` (`tbt.LaserPatternMode`): The laser pattern mode to set.
300 ## Returns
302 - `bool`: True if the pattern mode is set correctly.
304 ## Raises
306 - `SystemError`: If the pattern mode cannot be set correctly.
307 """
308 tfs_laser.Patterning_Mode(mode.value)
309 laser_state = factory.active_laser_state()
310 if laser_state.pattern.mode != mode: 310 ↛ 312line 310 didn't jump to line 312 because the condition on line 310 was always true
311 raise SystemError("Unable to correctly set pattern mode.")
312 return True
315def pulse_energy_uj(
316 energy_uj: float,
317 energy_tol_uj: float = Constants.laser_energy_tol_uj,
318 delay_s: float = 3.0,
319) -> bool:
320 """
321 Set the pulse energy on the laser.
323 This function sets the pulse energy on the laser and verifies that it has been set correctly. It should be done after setting the pulse divider.
325 ## Parameters
327 - `energy_uj` (`float`): The pulse energy to set in microjoules.
328 - `energy_tol_uj` (`float, optional`): The tolerance for the pulse energy in microjoules (default is Constants.laser_energy_tol_uj).
329 - `delay_s` (`float, optional`): The delay in seconds after setting the pulse energy (default is 3.0 seconds).
331 ## Returns
333 - `bool`: True if the pulse energy is set correctly.
335 ## Raises
337 - `ValueError`: If the pulse energy cannot be set correctly.
338 """
339 tfs_laser.Laser_SetPulseEnergy_MicroJoules(energy_uj)
340 time.sleep(delay_s)
341 laser_state = factory.active_laser_state()
342 if not ut.in_interval(
343 val=laser_state.pulse_energy_uj,
344 limit=tbt.Limit(
345 min=energy_uj - energy_tol_uj,
346 max=energy_uj + energy_tol_uj,
347 ),
348 type=tbt.IntervalType.CLOSED,
349 ):
350 raise ValueError(
351 f"Could not properly set pulse energy, requested '{energy_uj}' uJ",
352 f"Current settings is {round(laser_state.pulse_energy_uj, 3)} uJ",
353 )
354 return True
357def pulse_divider(
358 divider: int,
359 delay_s: float = Constants.laser_delay_s,
360) -> bool:
361 """
362 Set the pulse divider on the laser.
364 This function sets the pulse divider on the laser and verifies that it has been set correctly.
366 ## Parameters
368 - `divider` (`int`): The pulse divider to set.
369 - `delay_s` (`float, optional`): The delay in seconds after setting the pulse divider (default is Constants.laser_delay_s).
371 ## Returns
373 - `bool`: True if the pulse divider is set correctly.
375 ## Raises
377 - `ValueError`: If the pulse divider cannot be set correctly.
378 """
379 tfs_laser.Laser_PulseDivider(divider)
380 time.sleep(delay_s)
381 laser_state = factory.active_laser_state()
382 if laser_state.pulse_divider != divider:
383 raise ValueError(
384 f"Could not properly set pulse divider, requested '{divider}'",
385 f"Current settings have a divider of {laser_state.pulse_divider}.",
386 )
387 return True
390def set_wavelength(
391 wavelength: tbt.LaserWavelength,
392 frequency_khz: float = 60, # make constnat
393 timeout_s: int = 20, # 120, # make constant
394 num_attempts: int = 2, # TODO make a constant
395 delay_s: int = 5, # make a constant
396) -> bool:
397 """
398 Set the wavelength and frequency of the laser.
400 This function sets the wavelength and frequency of the laser and verifies that they have been set correctly.
402 ## Parameters
404 - `wavelength` (`tbt.LaserWavelength`): The wavelength to set.
405 - `frequency_khz` (`float, optional`): The frequency to set in kHz (default is 60 kHz).
406 - `timeout_s` (`int, optional`): The timeout in seconds for each attempt (default is 20 seconds).
407 - `num_attempts` (`int, optional`): The number of attempts to set the wavelength and frequency (default is 2).
408 - `delay_s` (`int, optional`): The delay in seconds between checks (default is 5 seconds).
410 ## Returns
412 - `bool`: True if the wavelength and frequency are set correctly, False otherwise.
413 """
415 def correct_preset(laser_state: tbt.LaserState):
416 if laser_state.wavelength_nm == wavelength:
417 return math.isclose(laser_state.frequency_khz, frequency_khz, rel_tol=0.05)
418 # TODO use constant for tolerance):
419 return False
421 for _ in range(num_attempts):
422 if correct_preset(factory.active_laser_state()): 422 ↛ 424line 422 didn't jump to line 424 because the condition on line 422 was always true
423 return True
424 print("Adjusting preset...")
425 tfs_laser.Laser_SetPreset(
426 wavelength_nm=wavelength.value, frequency_kHz=frequency_khz
427 )
428 time_remaining = timeout_s
429 while time_remaining > 0:
430 laser_state = factory.active_laser_state()
431 # print(time_remaining, laser_state.frequency_khz)
432 if correct_preset(laser_state=laser_state):
433 return True
434 time.sleep(delay_s)
435 time_remaining -= delay_s
437 # TODO: This does not verify that the wavelength was set and does not match the other functions here
438 # Perhaps this should be modified to raise an error if it does not set?
439 # Might be a lase API thing though?
440 return False
443def read_power(delay_s: float = Constants.laser_delay_s) -> float:
444 """
445 Measure the laser power in watts.
447 This function measures the laser power using an external power meter.
449 ## Parameters
451 - `delay_s` (`float, optional`): The delay in seconds before reading the power (default is Constants.laser_delay_s).
453 ## Returns
455 - `float`: The measured laser power in watts.
456 """
457 # TODO: Perhaps a try/finally structure would be safer here?
458 # Unless the laser API ensures that emission is off if it fails?
459 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON()
460 # try:
461 # tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset()
462 # tfs_laser.Laser_FireContinuously_Start()
463 # try:
464 # time.sleep(delay_s)
465 # return tfs_laser.Laser_ExternalPowerMeter_ReadPower()
466 # finally:
467 # tfs_laser.Laser_FireContinuously_Stop()
468 # finally:
469 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF()
470 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON()
471 tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset()
472 tfs_laser.Laser_FireContinuously_Start()
473 time.sleep(delay_s)
474 power = tfs_laser.Laser_ExternalPowerMeter_ReadPower()
475 tfs_laser.Laser_FireContinuously_Stop()
476 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF()
477 return power
480def insert_shutter(microscope: tbt.Microscope) -> bool:
481 """
482 Insert the laser shutter.
484 This function inserts the laser shutter and verifies that it has been inserted correctly.
486 ## Parameters
488 - `microscope` (`tbt.Microscope`): The microscope object for which to insert the laser shutter.
490 ## Returns
492 - `bool`: True if the laser shutter is successfully inserted.
494 ## Raises
496 - `SystemError`: If the laser shutter cannot be inserted.
497 """
498 devices.CCD_view(microscope=microscope)
499 if tfs_laser.Shutter_GetState() != "Inserted": 499 ↛ 500, 499 ↛ 5012 missed branches: 1) line 499 didn't jump to line 500 because the condition on line 499 was never true, 2) line 499 didn't jump to line 501 because the condition on line 499 was always true
500 tfs_laser.Shutter_Insert()
501 state = tfs_laser.Shutter_GetState()
502 if state != "Inserted":
503 raise SystemError(
504 f"Could not insert laser shutter, current laser shutter state is '{state}'."
505 )
506 devices.CCD_pause(microscope=microscope)
507 return True
510def retract_shutter(microscope: tbt.Microscope) -> bool:
511 """
512 Retract the laser shutter.
514 This function retracts the laser shutter and verifies that it has been retracted correctly.
516 ## Parameters
518 - `microscope` (`tbt.Microscope`): The microscope object for which to retract the laser shutter.
520 ## Returns
522 - `bool`: True if the laser shutter is successfully retracted.
524 ## Raises
526 - `SystemError`: If the laser shutter cannot be retracted.
527 """
528 devices.CCD_view(microscope=microscope)
529 if tfs_laser.Shutter_GetState() != "Retracted":
530 tfs_laser.Shutter_Retract()
531 state = tfs_laser.Shutter_GetState()
532 if state != "Retracted": 532 ↛ 536line 532 didn't jump to line 536 because the condition on line 532 was always true
533 raise SystemError(
534 f"Could not retract laser shutter, current laser shutter state is '{state}'."
535 )
536 devices.CCD_pause(microscope=microscope)
537 return True
540def pulse_polarization(
541 polarization: tbt.LaserPolarization, wavelength: tbt.LaserWavelength
542) -> bool:
543 """
544 Configure the polarization of the laser light.
546 This function sets the polarization of the laser light based on the specified polarization and wavelength. The polarization is controlled via "FlipperConfiguration", which takes the following values:
547 - Waveplate_None switches to Vert. (P)
548 - Waveplate_1030 switches to Horiz. (S)
549 - Waveplate_515 switches to Horiz. (S)
551 ## Parameters
553 - `polarization` (`tbt.LaserPolarization`): The desired polarization of the laser light.
554 - `wavelength` (`tbt.LaserWavelength`): The wavelength of the laser light.
556 ## Returns
558 - `bool`: True if the polarization is set correctly.
560 ## Raises
562 - `KeyError`: If the laser wavelength or pulse polarization is invalid.
563 """
564 if polarization == tbt.LaserPolarization.VERTICAL:
565 tfs_laser.FlipperConfiguration("Waveplate_None")
566 return True
567 elif polarization == tbt.LaserPolarization.HORIZONTAL:
568 match_db = {
569 tbt.LaserWavelength.NM_1030: "Waveplate_1030",
570 tbt.LaserWavelength.NM_515: "Waveplate_515",
571 }
572 try:
573 tfs_laser.FlipperConfiguration(match_db[wavelength])
574 except KeyError:
575 raise KeyError(
576 f"Invalid laser wavelength, valid options are {[i.value for i in tbt.LaserWavelength]}"
577 )
578 return True
579 else:
580 raise KeyError(
581 f"Invalid pulse polarization, valid options are {[i.value for i in tbt.LaserPolarization]}"
582 )
585def pulse_settings(pulse: tbt.LaserPulse) -> bool:
586 """
587 Apply the pulse settings to the laser.
589 This function applies the specified pulse settings to the laser, including wavelength, pulse divider, pulse energy, and polarization.
591 ## Parameters
593 - `pulse` (`tbt.LaserPulse`): The pulse settings to apply.
595 ## Returns
597 - `bool`: True if the pulse settings are applied correctly.
598 """
599 active_state = factory.active_laser_state()
600 if pulse.wavelength_nm != active_state.wavelength_nm: 600 ↛ 602, 600 ↛ 6032 missed branches: 1) line 600 didn't jump to line 602 because the condition on line 600 was never true, 2) line 600 didn't jump to line 603 because the condition on line 600 was always true
601 # wavelength settings
602 set_wavelength(wavelength=pulse.wavelength_nm)
603 pulse_divider(divider=pulse.divider)
604 pulse_energy_uj(energy_uj=pulse.energy_uj)
605 pulse_polarization(polarization=pulse.polarization, wavelength=pulse.wavelength_nm)
606 return True
609def retract_laser_objective() -> bool:
610 """
611 Retract the laser objective to a safe position.
613 This function retracts the laser objective to a predefined safe position.
615 ## Returns
617 bool
618 True if the laser objective is successfully retracted.
620 """
621 objective_position(position_mm=Constants.laser_objective_retracted_mm)
622 return True
625def objective_position(
626 position_mm: float,
627 tolerance_mm=Constants.laser_objective_tolerance_mm,
628) -> bool:
629 """
630 Move the laser objective to the requested position.
632 This function moves the laser objective to the specified position and verifies that it has been moved correctly.
634 ## Parameters
636 - `position_mm` (`float`): The desired position of the laser objective in millimeters.
637 - `tolerance_mm` (`float, optional`): The tolerance for the laser objective position in millimeters (default is Constants.laser_objective_tolerance_mm).
639 ## Returns
641 - `bool`: True if the laser objective is moved to the requested position correctly.
643 ## Raises
645 - `ValueError`: If the requested position is out of range.
646 - `SystemError`: If the laser objective cannot be moved to the requested position.
647 """
648 tfs_laser.LIP_UnlockZ()
650 if not ut.in_interval( 650 ↛ 659line 650 didn't jump to line 659 because the condition on line 650 was always true
651 val=position_mm,
652 limit=Constants.laser_objective_limit_mm,
653 type=tbt.IntervalType.CLOSED,
654 ):
655 raise ValueError(
656 f"Requested laser objective position of {position_mm} mm is out of range. Laser objective can travel from {Constants.laser_objective_limit_mm.min} to {Constants.laser_objective_limit_mm.max} mm."
657 )
659 for _ in range(2):
660 if ut.in_interval( 660 ↛ 667, 660 ↛ 6682 missed branches: 1) line 660 didn't jump to line 667 because the condition on line 660 was never true, 2) line 660 didn't jump to line 668 because the condition on line 660 was always true
661 val=tfs_laser.LIP_GetZPosition(),
662 limit=tbt.Limit(
663 min=position_mm - tolerance_mm, max=position_mm + tolerance_mm
664 ),
665 type=tbt.IntervalType.CLOSED,
666 ):
667 return True
668 tfs_laser.LIP_SetZPosition(position_mm, asynchronously=False)
670 raise SystemError(
671 f"Unable to move laser injection port objective to requested position of {position_mm} +/- {tolerance_mm} mm.",
672 f"Currently at {tfs_laser.LIP_GetZPosition()} mm.",
673 )
676def _shift_axis(
677 target: float,
678 current: float,
679 tolerance: float,
680 axis: str,
681) -> bool:
682 """
683 Helper function for beam shift.
685 This function adjusts the beam shift for the specified axis to the target value within the given tolerance.
687 ## Parameters
689 - `target` (`float`): The target value for the beam shift.
690 - `current` (`float`): The current value of the beam shift.
691 - `tolerance` (`float`): The tolerance for the beam shift.
692 - `axis` (`str`): The axis to adjust ("X" or "Y").
694 ## Returns
696 - `bool`: True if the beam shift is adjusted to the target value correctly, False otherwise.
697 """
698 for _ in range(2): 698 ↛ 715line 698 didn't jump to line 715 because the loop on line 698 didn't complete
699 if ut.in_interval( 699 ↛ 707, 699 ↛ 7082 missed branches: 1) line 699 didn't jump to line 707 because the condition on line 699 was never true, 2) line 699 didn't jump to line 708 because the condition on line 699 was always true
700 val=current,
701 limit=tbt.Limit(
702 min=target - tolerance,
703 max=target + tolerance,
704 ),
705 type=tbt.IntervalType.CLOSED,
706 ):
707 return True
708 if axis == "X":
709 tfs_laser.BeamShift_Set_X(value=target)
710 current = tfs_laser.BeamShift_Get_X()
711 if axis == "Y":
712 tfs_laser.BeamShift_Set_Y(value=target)
713 current = tfs_laser.BeamShift_Get_Y()
715 return False
718def beam_shift(
719 shift_um: tbt.Point,
720 shift_tolerance_um: float = Constants.laser_beam_shift_tolerance_um,
721) -> bool:
722 """
723 Adjust the laser beam shift to the specified values.
725 This function adjusts the laser beam shift to the specified x and y values within the given tolerance.
727 ## Parameters
729 - `shift_um` (`tbt.Point`): The target beam shift values in micrometers.
730 - `shift_tolerance_um` (`float, optional`): The tolerance for the beam shift in micrometers (default is Constants.laser_beam_shift_tolerance_um).
732 ## Returns
734 - `bool`: True if the beam shift is adjusted to the target values correctly.
736 ## Raises
738 - `ValueError`: If the beam shift cannot be adjusted to the target values.
739 """
740 current_shift_x = tfs_laser.BeamShift_Get_X()
741 current_shift_y = tfs_laser.BeamShift_Get_Y()
743 if not ( 743 ↛ 760line 743 didn't jump to line 760 because the condition on line 743 was always true
744 _shift_axis(
745 target=shift_um.x,
746 current=current_shift_x,
747 tolerance=shift_tolerance_um,
748 axis="X",
749 )
750 and _shift_axis(
751 target=shift_um.y,
752 current=current_shift_y,
753 tolerance=shift_tolerance_um,
754 axis="Y",
755 )
756 ):
757 raise ValueError(
758 f"Unable to set laser beam shift. Requested beam shift of (x,y) = ({shift_um.x} um,{shift_um.y} um,), but current beam shift is ({tfs_laser.BeamShift_Get_X()} um, {tfs_laser.BeamShift_Get_Y()} um)."
759 )
760 return True
763def create_pattern(pattern: tbt.LaserPattern) -> bool:
764 """
765 Create a laser pattern and check that it is set correctly.
767 This function creates a laser pattern based on the specified pattern settings and verifies that it has been set correctly.
769 ## Parameters
771 - `pattern` (`tbt.LaserPattern`): The laser pattern settings to create.
773 ## Returns
775 - `bool`: True if the pattern is created and set correctly.
777 ## Raises
779 - `ValueError`: If the pattern geometry type is unsupported.
780 - `SystemError`: If the pattern cannot be set correctly.
781 """
782 pattern_mode(mode=pattern.mode)
784 # check if pattern is empty or not
785 if isinstance(pattern.geometry, tbt.LaserBoxPattern):
786 box = pattern.geometry
787 tfs_laser.Patterning_CreatePattern_Box(
788 sizeX_um=box.size_x_um,
789 sizeY_um=box.size_y_um,
790 pitchX_um=box.pitch_x_um,
791 pitchY_um=box.pitch_y_um,
792 dwellTime_ms=pattern.pixel_dwell_ms,
793 passes_int=box.passes,
794 pulsesPerPixel_int=pattern.pulses_per_pixel,
795 scanrotation_degrees=pattern.rotation_deg,
796 scantype_string=box.scan_type.value, # cast enum to string
797 coordinateReference_string=box.coordinate_ref.value, # cast enum to string
798 )
799 elif isinstance(pattern.geometry, tbt.LaserLinePattern):
800 line = pattern.geometry
801 tfs_laser.Patterning_CreatePattern_Line(
802 sizeX_um=line.size_um,
803 pitchX_um=line.pitch_um,
804 dwellTime_ms=pattern.pixel_dwell_ms,
805 passes_int=line.passes,
806 pulsesPerPixel_int=pattern.pulses_per_pixel,
807 scanrotation_degrees=pattern.rotation_deg,
808 scantype_string=line.scan_type.value, # cast enum to string
809 )
810 else:
811 raise ValueError(
812 f"Unsupported pattern geometry of type '{type(pattern.geometry)}'. Supported types are {tbt.LaserLinePattern, tbt.LaserBoxPattern}"
813 )
814 laser_state = factory.active_laser_state()
815 if laser_state.pattern != pattern:
816 raise SystemError("Unable to correctly set Pattern.")
817 return True
820def apply_laser_settings(image_beam: tbt.Beam, settings: tbt.LaserSettings) -> bool:
821 """
822 Apply the laser settings to the current patterning.
824 This function applies the specified laser settings to the current patterning, including beam scan rotation, pulse settings, objective position, beam shift, and patterning settings.
826 ## Parameters
828 - `image_beam` (`tbt.Beam`): The beam settings for the image.
829 - `settings` (`tbt.LaserSettings`): The laser settings to apply.
831 ## Returns
833 - `bool`: True if the laser settings are applied correctly.
834 """
835 microscope = settings.microscope
837 # forces rotation of electron beam for laser (TFS required)
838 img.beam_scan_rotation(
839 beam=image_beam,
840 microscope=microscope,
841 rotation_deg=Constants.image_scan_rotation_for_laser_deg,
842 )
843 # pulse settings
844 pulse_settings(pulse=settings.pulse)
846 # objective position
847 objective_position(settings.objective_position_mm)
849 # beam shift
850 beam_shift(settings.beam_shift_um)
852 # apply patterning settings
853 create_pattern(pattern=settings.pattern)
855 return True
858def execute_patterning() -> bool:
859 """
860 Execute the laser patterning.
862 This function starts the laser patterning process.
864 ## Returns
866 bool
867 True if the patterning process is started successfully.
869 """
870 tfs_laser.Patterning_Start()
872 return True
875### main methods
878def mill_region(
879 settings: tbt.LaserSettings,
880) -> bool:
881 """
882 Perform laser milling on a specified region.
884 This function performs laser milling on a specified region using the provided laser settings. It checks the laser connection, applies the laser settings, inserts the shutter, executes the patterning, retracts the shutter, and resets the scan rotation.
886 ## Parameters
888 - `settings` (`tbt.LaserSettings`): The laser settings to use for milling.
890 ## Returns
892 - `bool`: True if the milling process is completed successfully.
894 ## Raises
896 - `SystemError`: If the laser is not connected.
897 """
898 # check connection
899 if not laser_connected():
900 raise SystemError("Laser is not connected")
902 microscope = settings.microscope
903 # initial_scan_rotation of ebeam
904 devices.device_access(microscope=microscope)
905 active_beam = factory.active_beam_with_settings(microscope=microscope)
906 scan_settings = factory.active_scan_settings(microscope=microscope)
907 initial_scan_rotation_deg = scan_settings.rotation_deg
909 # apply laser settings
910 apply_laser_settings(
911 image_beam=active_beam,
912 settings=settings,
913 )
915 # insert shutter
916 insert_shutter(microscope=microscope)
918 # execute patterning
919 execute_patterning()
921 # retract shutter
922 retract_shutter(microscope=microscope)
923 time.sleep(1)
925 # reset scan rotation
926 img.beam_scan_rotation(
927 beam=active_beam,
928 microscope=microscope,
929 rotation_deg=initial_scan_rotation_deg,
930 )
932 # TODO: Current code is not very safe here
933 # Below makes sure that the scan reset happens regardless of what happens during milling
934 # try:
935 # apply_laser_settings(
936 # image_beam=active_beam,
937 # settings=settings,
938 # )
939 # insert_shutter(microscope=microscope)
940 # execute_patterning()
941 # finally:
942 # try:
943 # retract_shutter(microscope=microscope)
944 # finally:
945 # img.beam_scan_rotation(
946 # beam=active_beam,
947 # microscope=microscope,
948 # rotation_deg=initial_scan_rotation_deg,
949 # )
951 return True
954def laser_operation(
955 step: tbt.Step, general_settings: tbt.GeneralSettings, slice_number: int
956) -> bool:
957 """
958 Perform a laser operation based on the specified step and settings.
960 This function performs a laser operation using the provided step and general settings. It logs the laser power before and after the operation, and performs the milling process.
962 ## Parameters
964 - `step` (`tbt.Step`): The step object containing the operation settings.
965 - `general_settings` (`tbt.GeneralSettings`): The general settings object.
966 - `slice_number` (`int`): The slice number for the operation.
968 ## Returns
970 - `bool`: True if the laser operation is completed successfully.
971 """
972 # log laser power before
973 laser_power_w = read_power()
974 log.laser_power(
975 step_number=step.number,
976 step_name=step.name,
977 slice_number=slice_number,
978 log_filepath=general_settings.log_filepath,
979 dataset_name=Constants.pre_lasing_dataset_name,
980 power_w=laser_power_w,
981 )
983 mill_region(settings=step.operation_settings)
985 # log laser power after
986 laser_power_w = read_power()
987 log.laser_power(
988 step_number=step.number,
989 step_name=step.name,
990 slice_number=slice_number,
991 log_filepath=general_settings.log_filepath,
992 dataset_name=Constants.post_lasing_dataset_name,
993 power_w=laser_power_w,
994 )
996 return True
999def map_ebsd() -> bool:
1000 """
1001 Start an EBSD map and ensure it takes the minimum expected time.
1003 This function starts an EBSD map and checks that the mapping process takes at least the minimum expected time. If the mapping process is too short, it raises an error.
1005 ## Returns
1007 bool
1008 True if the EBSD mapping is completed successfully.
1010 ## Raises
1012 - `ValueError`: If the mapping process does not take the minimum expected time.
1013 """
1014 start_time = time.time()
1015 tfs_laser.EBSD_StartMap()
1016 time.sleep(1)
1017 end_time = time.time()
1018 map_time = end_time - start_time
1019 if map_time < Constants.min_map_time_s:
1020 raise ValueError(
1021 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EBSD mapping software"
1022 )
1023 print(f"\t\tMapping Complete in {int(map_time)} seconds.")
1024 return True
1027def map_eds() -> bool:
1028 """
1029 Start an EDS map and ensure it takes the minimum expected time.
1031 This function starts an EDS map and checks that the mapping process takes at least the minimum expected time. If the mapping process is too short, it raises an error.
1033 ## Returns
1035 bool
1036 True if the EDS mapping is completed successfully.
1038 ## Raises
1040 - `ValueError`: If the mapping process does not take the minimum expected time.
1041 """
1042 start_time = time.time()
1043 tfs_laser.EDS_StartMap()
1044 time.sleep(1)
1045 end_time = time.time()
1046 map_time = end_time - start_time
1047 if map_time < Constants.min_map_time_s:
1048 raise ValueError(
1049 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EDS mapping software"
1050 )
1051 print(f"\t\tMapping Complete in {int(map_time)} seconds.")
1052 return True