Coverage for src/pytribeam/laser.py: 69%
253 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-10-14 19:37 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-10-14 19:37 +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
164from typing import Optional
166try:
167 import Laser.PythonControl as tfs_laser
168except ImportError:
169 tfs_laser = None
170 print("WARNING: Laser API not imported!")
171 print("\tLaser control, as well as EBSD and EDS control are unavailable.")
172else:
173 print("Laser PythonControl API imported.")
175# 3rd party .whl modules
177# Local scripts
178from pytribeam.constants import Constants
179import pytribeam.factory as factory
180import pytribeam.types as tbt
181import pytribeam.utilities as ut
182import pytribeam.insertable_devices as devices
183import pytribeam.image as img
184import pytribeam.log as log
187def laser_state_to_db(state: tbt.LaserState) -> dict:
188 """
189 This function converts a `LaserState` object into a flattened dictionary representation.
191 ## Parameters
193 - `state` (`tbt.LaserState`): The laser state object to convert.
195 ## Returns
197 - `dict`: A flattened dictionary representation of the laser state.
198 """
199 db = {}
201 db["wavelength_nm"] = state.wavelength_nm
202 db["frequency_khz"] = state.frequency_khz
203 db["pulse_divider"] = state.pulse_divider
204 db["pulse_energy_uj"] = state.pulse_energy_uj
205 db["objective_position_mm"] = state.objective_position_mm
206 db["expected_pattern_duration_s"] = state.expected_pattern_duration_s
208 beam_shift = state.beam_shift_um
209 db["beam_shift_um_x"] = beam_shift.x
210 db["beam_shift_um_y"] = beam_shift.y
212 # we can name these differently depending on the needs of the GUI
213 pattern = state.pattern
214 db["laser_pattern_mode"] = pattern.mode.value
215 db["laser_pattern_rotation_deg"] = pattern.rotation_deg
216 db["laser_pattern_pulses_per_pixel"] = pattern.pulses_per_pixel
217 db["laser_pattern_pixel_dwell_ms"] = pattern.pixel_dwell_ms
219 geometry = pattern.geometry
220 db["passes"] = geometry.passes
221 db["laser_scan_type"] = geometry.scan_type.value
222 db["geometry_type"] = geometry.type.value
224 if geometry.type == tbt.LaserPatternType.BOX:
225 db["size_x_um"] = geometry.size_x_um
226 db["size_y_um"] = geometry.size_y_um
227 db["pitch_x_um"] = geometry.pitch_x_um
228 db["pitch_y_um"] = geometry.pitch_y_um
229 db["coordinate_ref"] = geometry.coordinate_ref
231 if geometry.type == tbt.LaserPatternType.LINE:
232 db["size_um"] = geometry.size_um
233 db["pitch_um"] = geometry.pitch_um
235 return db
238def laser_connected() -> bool:
239 """
240 Check if the laser is connected.
242 This function tests the connection to the laser and returns True if the connection is successful.
244 ## Returns
246 bool
247 True if the laser is connected, False otherwise.
249 """
250 connect_msg = "Connection test successful.\n"
251 laser_status = io.StringIO()
252 try:
253 with contextlib.redirect_stdout(laser_status):
254 tfs_laser.TestConnection()
255 except:
256 return False
257 else:
258 if laser_status.getvalue() == connect_msg: 258 ↛ 260line 258 didn't jump to line 260 because the condition on line 258 was always true
259 return True
260 return False
263def _device_connections(
264 microscope: Optional[tbt.Microscope] = None,
265) -> tbt.DeviceStatus:
266 """
267 Check the connection status of the laser and associated external devices.
269 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.
271 ## Returns
273 tbt.DeviceStatus
274 The connection status of the laser, EBSD, and EDS devices.
276 """
277 # laser must be connected to connect with other devices:
278 if not laser_connected(): 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 laser = tbt.RetractableDeviceState.ERROR
280 ebsd = tbt.RetractableDeviceState.ERROR
281 eds = tbt.RetractableDeviceState.ERROR
282 else:
283 laser = tbt.RetractableDeviceState.CONNECTED
284 ebsd = devices.connect_EBSD() # retractable device state
285 eds = devices.connect_EDS() # retractable device state
287 if microscope is not None: 287 ↛ 288line 287 didn't jump to line 288 because the condition on line 287 was never true
288 states = img.get_available_insertable_detector_states(microscope)
289 states = {a.value: b for (a, b) in states}
290 else:
291 states = {}
293 return tbt.DeviceStatus(
294 laser=laser,
295 ebsd=ebsd,
296 eds=eds,
297 extra_devices=states,
298 )
301def pattern_mode(mode: tbt.LaserPatternMode) -> bool:
302 """
303 Set the laser pattern mode.
305 This function sets the laser pattern mode and verifies that it has been set correctly.
307 ## Parameters
309 - `mode` (`tbt.LaserPatternMode`): The laser pattern mode to set.
311 ## Returns
313 - `bool`: True if the pattern mode is set correctly.
315 ## Raises
317 - `SystemError`: If the pattern mode cannot be set correctly.
318 """
319 tfs_laser.Patterning_Mode(mode.value)
320 laser_state = factory.active_laser_state()
321 if laser_state.pattern.mode != mode: 321 ↛ 322line 321 didn't jump to line 322 because the condition on line 321 was never true
322 raise SystemError("Unable to correctly set pattern mode.")
323 return True
326def pulse_energy_uj(
327 energy_uj: float,
328 energy_tol_uj: float = Constants.laser_energy_tol_uj,
329 delay_s: float = 3.0,
330) -> bool:
331 """
332 Set the pulse energy on the laser.
334 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.
336 ## Parameters
338 - `energy_uj` (`float`): The pulse energy to set in microjoules.
339 - `energy_tol_uj` (`float, optional`): The tolerance for the pulse energy in microjoules (default is Constants.laser_energy_tol_uj).
340 - `delay_s` (`float, optional`): The delay in seconds after setting the pulse energy (default is 3.0 seconds).
342 ## Returns
344 - `bool`: True if the pulse energy is set correctly.
346 ## Raises
348 - `ValueError`: If the pulse energy cannot be set correctly.
349 """
350 tfs_laser.Laser_SetPulseEnergy_MicroJoules(energy_uj)
351 time.sleep(delay_s)
352 laser_state = factory.active_laser_state()
353 if not ut.in_interval(
354 val=laser_state.pulse_energy_uj,
355 limit=tbt.Limit(
356 min=energy_uj - energy_tol_uj,
357 max=energy_uj + energy_tol_uj,
358 ),
359 type=tbt.IntervalType.CLOSED,
360 ):
361 raise ValueError(
362 f"Could not properly set pulse energy, requested '{energy_uj}' uJ",
363 f"Current settings is {round(laser_state.pulse_energy_uj, 3)} uJ",
364 )
365 return True
368def pulse_divider(
369 divider: int,
370 delay_s: float = Constants.laser_delay_s,
371) -> bool:
372 """
373 Set the pulse divider on the laser.
375 This function sets the pulse divider on the laser and verifies that it has been set correctly.
377 ## Parameters
379 - `divider` (`int`): The pulse divider to set.
380 - `delay_s` (`float, optional`): The delay in seconds after setting the pulse divider (default is Constants.laser_delay_s).
382 ## Returns
384 - `bool`: True if the pulse divider is set correctly.
386 ## Raises
388 - `ValueError`: If the pulse divider cannot be set correctly.
389 """
390 tfs_laser.Laser_PulseDivider(divider)
391 time.sleep(delay_s)
392 laser_state = factory.active_laser_state()
393 if laser_state.pulse_divider != divider:
394 raise ValueError(
395 f"Could not properly set pulse divider, requested '{divider}'",
396 f"Current settings have a divider of {laser_state.pulse_divider}.",
397 )
398 return True
401def set_wavelength(
402 wavelength: tbt.LaserWavelength,
403 frequency_khz: float = 60, # make constnat
404 timeout_s: int = 20, # 120, # make constant
405 num_attempts: int = 2, # TODO make a constant
406 delay_s: int = 5, # make a constant
407) -> bool:
408 """
409 Set the wavelength and frequency of the laser.
411 This function sets the wavelength and frequency of the laser and verifies that they have been set correctly.
413 ## Parameters
415 - `wavelength` (`tbt.LaserWavelength`): The wavelength to set.
416 - `frequency_khz` (`float, optional`): The frequency to set in kHz (default is 60 kHz).
417 - `timeout_s` (`int, optional`): The timeout in seconds for each attempt (default is 20 seconds).
418 - `num_attempts` (`int, optional`): The number of attempts to set the wavelength and frequency (default is 2).
419 - `delay_s` (`int, optional`): The delay in seconds between checks (default is 5 seconds).
421 ## Returns
423 - `bool`: True if the wavelength and frequency are set correctly, False otherwise.
424 """
426 def correct_preset(laser_state: tbt.LaserState):
427 if laser_state.wavelength_nm == wavelength:
428 return math.isclose(laser_state.frequency_khz, frequency_khz, rel_tol=0.05)
429 # TODO use constant for tolerance):
430 return False
432 for _ in range(num_attempts): 432 ↛ 451line 432 didn't jump to line 451 because the loop on line 432 didn't complete
433 if correct_preset(factory.active_laser_state()):
434 return True
435 print("Adjusting preset...")
436 tfs_laser.Laser_SetPreset(
437 wavelength_nm=wavelength.value, frequency_kHz=frequency_khz
438 )
439 time_remaining = timeout_s
440 while time_remaining > 0: 440 ↛ 432line 440 didn't jump to line 432 because the condition on line 440 was always true
441 laser_state = factory.active_laser_state()
442 # print(time_remaining, laser_state.frequency_khz)
443 if correct_preset(laser_state=laser_state):
444 return True
445 time.sleep(delay_s)
446 time_remaining -= delay_s
448 # TODO: This does not verify that the wavelength was set and does not match the other functions here
449 # Perhaps this should be modified to raise an error if it does not set?
450 # Might be a lase API thing though?
451 return False
454def read_power(delay_s: float = Constants.laser_delay_s) -> float:
455 """
456 Measure the laser power in watts.
458 This function measures the laser power using an external power meter.
460 ## Parameters
462 - `delay_s` (`float, optional`): The delay in seconds before reading the power (default is Constants.laser_delay_s).
464 ## Returns
466 - `float`: The measured laser power in watts.
467 """
468 # TODO: Perhaps a try/finally structure would be safer here?
469 # Unless the laser API ensures that emission is off if it fails?
470 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON()
471 # try:
472 # tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset()
473 # tfs_laser.Laser_FireContinuously_Start()
474 # try:
475 # time.sleep(delay_s)
476 # return tfs_laser.Laser_ExternalPowerMeter_ReadPower()
477 # finally:
478 # tfs_laser.Laser_FireContinuously_Stop()
479 # finally:
480 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF()
481 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON()
482 tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset()
483 tfs_laser.Laser_FireContinuously_Start()
484 time.sleep(delay_s)
485 power = tfs_laser.Laser_ExternalPowerMeter_ReadPower()
486 tfs_laser.Laser_FireContinuously_Stop()
487 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF()
488 return power
491def insert_shutter(microscope: tbt.Microscope) -> bool:
492 """
493 Insert the laser shutter.
495 This function inserts the laser shutter and verifies that it has been inserted correctly.
497 ## Parameters
499 - `microscope` (`tbt.Microscope`): The microscope object for which to insert the laser shutter.
501 ## Returns
503 - `bool`: True if the laser shutter is successfully inserted.
505 ## Raises
507 - `SystemError`: If the laser shutter cannot be inserted.
508 """
509 devices.CCD_view(microscope=microscope)
510 if tfs_laser.Shutter_GetState() != "Inserted": 510 ↛ 512line 510 didn't jump to line 512 because the condition on line 510 was always true
511 tfs_laser.Shutter_Insert()
512 state = tfs_laser.Shutter_GetState()
513 if state != "Inserted": 513 ↛ 514line 513 didn't jump to line 514 because the condition on line 513 was never true
514 raise SystemError(
515 f"Could not insert laser shutter, current laser shutter state is '{state}'."
516 )
517 devices.CCD_pause(microscope=microscope)
518 return True
521def retract_shutter(microscope: tbt.Microscope) -> bool:
522 """
523 Retract the laser shutter.
525 This function retracts the laser shutter and verifies that it has been retracted correctly.
527 ## Parameters
529 - `microscope` (`tbt.Microscope`): The microscope object for which to retract the laser shutter.
531 ## Returns
533 - `bool`: True if the laser shutter is successfully retracted.
535 ## Raises
537 - `SystemError`: If the laser shutter cannot be retracted.
538 """
539 devices.CCD_view(microscope=microscope)
540 if tfs_laser.Shutter_GetState() != "Retracted": 540 ↛ 542line 540 didn't jump to line 542 because the condition on line 540 was always true
541 tfs_laser.Shutter_Retract()
542 state = tfs_laser.Shutter_GetState()
543 if state != "Retracted": 543 ↛ 544line 543 didn't jump to line 544 because the condition on line 543 was never true
544 raise SystemError(
545 f"Could not retract laser shutter, current laser shutter state is '{state}'."
546 )
547 devices.CCD_pause(microscope=microscope)
548 return True
551def pulse_polarization(
552 polarization: tbt.LaserPolarization, wavelength: tbt.LaserWavelength
553) -> bool:
554 """
555 Configure the polarization of the laser light.
557 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:
558 - Waveplate_None switches to Vert. (P)
559 - Waveplate_1030 switches to Horiz. (S)
560 - Waveplate_515 switches to Horiz. (S)
562 ## Parameters
564 - `polarization` (`tbt.LaserPolarization`): The desired polarization of the laser light.
565 - `wavelength` (`tbt.LaserWavelength`): The wavelength of the laser light.
567 ## Returns
569 - `bool`: True if the polarization is set correctly.
571 ## Raises
573 - `KeyError`: If the laser wavelength or pulse polarization is invalid.
574 """
575 if polarization == tbt.LaserPolarization.VERTICAL:
576 tfs_laser.FlipperConfiguration("Waveplate_None")
577 return True
578 elif polarization == tbt.LaserPolarization.HORIZONTAL:
579 match_db = {
580 tbt.LaserWavelength.NM_1030: "Waveplate_1030",
581 tbt.LaserWavelength.NM_515: "Waveplate_515",
582 }
583 try:
584 tfs_laser.FlipperConfiguration(match_db[wavelength])
585 except KeyError:
586 raise KeyError(
587 f"Invalid laser wavelength, valid options are {[i.value for i in tbt.LaserWavelength]}"
588 )
589 return True
590 else:
591 raise KeyError(
592 f"Invalid pulse polarization, valid options are {[i.value for i in tbt.LaserPolarization]}"
593 )
596def pulse_settings(pulse: tbt.LaserPulse) -> bool:
597 """
598 Apply the pulse settings to the laser.
600 This function applies the specified pulse settings to the laser, including wavelength, pulse divider, pulse energy, and polarization.
602 ## Parameters
604 - `pulse` (`tbt.LaserPulse`): The pulse settings to apply.
606 ## Returns
608 - `bool`: True if the pulse settings are applied correctly.
609 """
610 active_state = factory.active_laser_state()
611 if pulse.wavelength_nm != active_state.wavelength_nm: 611 ↛ 613line 611 didn't jump to line 613 because the condition on line 611 was never true
612 # wavelength settings
613 set_wavelength(wavelength=pulse.wavelength_nm)
614 pulse_divider(divider=pulse.divider)
615 pulse_energy_uj(energy_uj=pulse.energy_uj)
616 pulse_polarization(polarization=pulse.polarization, wavelength=pulse.wavelength_nm)
617 return True
620def retract_laser_objective() -> bool:
621 """
622 Retract the laser objective to a safe position.
624 This function retracts the laser objective to a predefined safe position.
626 ## Returns
628 bool
629 True if the laser objective is successfully retracted.
631 """
632 objective_position(position_mm=Constants.laser_objective_retracted_mm)
633 return True
636def objective_position(
637 position_mm: float,
638 tolerance_mm=Constants.laser_objective_tolerance_mm,
639) -> bool:
640 """
641 Move the laser objective to the requested position.
643 This function moves the laser objective to the specified position and verifies that it has been moved correctly.
645 ## Parameters
647 - `position_mm` (`float`): The desired position of the laser objective in millimeters.
648 - `tolerance_mm` (`float, optional`): The tolerance for the laser objective position in millimeters (default is Constants.laser_objective_tolerance_mm).
650 ## Returns
652 - `bool`: True if the laser objective is moved to the requested position correctly.
654 ## Raises
656 - `ValueError`: If the requested position is out of range.
657 - `SystemError`: If the laser objective cannot be moved to the requested position.
658 """
659 tfs_laser.LIP_UnlockZ()
661 if not ut.in_interval( 661 ↛ 666line 661 didn't jump to line 666 because the condition on line 661 was never true
662 val=position_mm,
663 limit=Constants.laser_objective_limit_mm,
664 type=tbt.IntervalType.CLOSED,
665 ):
666 raise ValueError(
667 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."
668 )
670 for _ in range(2):
671 if ut.in_interval(
672 val=tfs_laser.LIP_GetZPosition(),
673 limit=tbt.Limit(
674 min=position_mm - tolerance_mm, max=position_mm + tolerance_mm
675 ),
676 type=tbt.IntervalType.CLOSED,
677 ):
678 return True
679 tfs_laser.LIP_SetZPosition(position_mm, asynchronously=False)
681 raise SystemError(
682 f"Unable to move laser injection port objective to requested position of {position_mm} +/- {tolerance_mm} mm.",
683 f"Currently at {tfs_laser.LIP_GetZPosition()} mm.",
684 )
687def _shift_axis(
688 target: float,
689 current: float,
690 tolerance: float,
691 axis: str,
692) -> bool:
693 """
694 Helper function for beam shift.
696 This function adjusts the beam shift for the specified axis to the target value within the given tolerance.
698 ## Parameters
700 - `target` (`float`): The target value for the beam shift.
701 - `current` (`float`): The current value of the beam shift.
702 - `tolerance` (`float`): The tolerance for the beam shift.
703 - `axis` (`str`): The axis to adjust ("X" or "Y").
705 ## Returns
707 - `bool`: True if the beam shift is adjusted to the target value correctly, False otherwise.
708 """
709 for _ in range(2): 709 ↛ 726line 709 didn't jump to line 726 because the loop on line 709 didn't complete
710 if ut.in_interval(
711 val=current,
712 limit=tbt.Limit(
713 min=target - tolerance,
714 max=target + tolerance,
715 ),
716 type=tbt.IntervalType.CLOSED,
717 ):
718 return True
719 if axis == "X":
720 tfs_laser.BeamShift_Set_X(value=target)
721 current = tfs_laser.BeamShift_Get_X()
722 if axis == "Y":
723 tfs_laser.BeamShift_Set_Y(value=target)
724 current = tfs_laser.BeamShift_Get_Y()
726 return False
729def beam_shift(
730 shift_um: tbt.Point,
731 shift_tolerance_um: float = Constants.laser_beam_shift_tolerance_um,
732) -> bool:
733 """
734 Adjust the laser beam shift to the specified values.
736 This function adjusts the laser beam shift to the specified x and y values within the given tolerance.
738 ## Parameters
740 - `shift_um` (`tbt.Point`): The target beam shift values in micrometers.
741 - `shift_tolerance_um` (`float, optional`): The tolerance for the beam shift in micrometers (default is Constants.laser_beam_shift_tolerance_um).
743 ## Returns
745 - `bool`: True if the beam shift is adjusted to the target values correctly.
747 ## Raises
749 - `ValueError`: If the beam shift cannot be adjusted to the target values.
750 """
751 current_shift_x = tfs_laser.BeamShift_Get_X()
752 current_shift_y = tfs_laser.BeamShift_Get_Y()
754 if not ( 754 ↛ 768line 754 didn't jump to line 768 because the condition on line 754 was never true
755 _shift_axis(
756 target=shift_um.x,
757 current=current_shift_x,
758 tolerance=shift_tolerance_um,
759 axis="X",
760 )
761 and _shift_axis(
762 target=shift_um.y,
763 current=current_shift_y,
764 tolerance=shift_tolerance_um,
765 axis="Y",
766 )
767 ):
768 raise ValueError(
769 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)."
770 )
771 return True
774def create_pattern(pattern: tbt.LaserPattern) -> bool:
775 """
776 Create a laser pattern and check that it is set correctly.
778 This function creates a laser pattern based on the specified pattern settings and verifies that it has been set correctly.
780 ## Parameters
782 - `pattern` (`tbt.LaserPattern`): The laser pattern settings to create.
784 ## Returns
786 - `bool`: True if the pattern is created and set correctly.
788 ## Raises
790 - `ValueError`: If the pattern geometry type is unsupported.
791 - `SystemError`: If the pattern cannot be set correctly.
792 """
793 pattern_mode(mode=pattern.mode)
795 # check if pattern is empty or not
796 if isinstance(pattern.geometry, tbt.LaserBoxPattern):
797 box = pattern.geometry
798 tfs_laser.Patterning_CreatePattern_Box(
799 sizeX_um=box.size_x_um,
800 sizeY_um=box.size_y_um,
801 pitchX_um=box.pitch_x_um,
802 pitchY_um=box.pitch_y_um,
803 dwellTime_ms=pattern.pixel_dwell_ms,
804 passes_int=box.passes,
805 pulsesPerPixel_int=pattern.pulses_per_pixel,
806 scanrotation_degrees=pattern.rotation_deg,
807 scantype_string=box.scan_type.value, # cast enum to string
808 coordinateReference_string=box.coordinate_ref.value, # cast enum to string
809 )
810 elif isinstance(pattern.geometry, tbt.LaserLinePattern): 810 ↛ 822line 810 didn't jump to line 822 because the condition on line 810 was always true
811 line = pattern.geometry
812 tfs_laser.Patterning_CreatePattern_Line(
813 sizeX_um=line.size_um,
814 pitchX_um=line.pitch_um,
815 dwellTime_ms=pattern.pixel_dwell_ms,
816 passes_int=line.passes,
817 pulsesPerPixel_int=pattern.pulses_per_pixel,
818 scanrotation_degrees=pattern.rotation_deg,
819 scantype_string=line.scan_type.value, # cast enum to string
820 )
821 else:
822 raise ValueError(
823 f"Unsupported pattern geometry of type '{type(pattern.geometry)}'. Supported types are {tbt.LaserLinePattern, tbt.LaserBoxPattern}"
824 )
825 laser_state = factory.active_laser_state()
826 if laser_state.pattern != pattern: 826 ↛ 827line 826 didn't jump to line 827 because the condition on line 826 was never true
827 raise SystemError("Unable to correctly set Pattern.")
828 return True
831def apply_laser_settings(image_beam: tbt.Beam, settings: tbt.LaserSettings) -> bool:
832 """
833 Apply the laser settings to the current patterning.
835 This function applies the specified laser settings to the current patterning, including beam scan rotation, pulse settings, objective position, beam shift, and patterning settings.
837 ## Parameters
839 - `image_beam` (`tbt.Beam`): The beam settings for the image.
840 - `settings` (`tbt.LaserSettings`): The laser settings to apply.
842 ## Returns
844 - `bool`: True if the laser settings are applied correctly.
845 """
846 microscope = settings.microscope
848 # forces rotation of electron beam for laser (TFS required)
849 img.beam_scan_rotation(
850 beam=image_beam,
851 microscope=microscope,
852 rotation_deg=Constants.image_scan_rotation_for_laser_deg,
853 )
854 # pulse settings
855 pulse_settings(pulse=settings.pulse)
857 # objective position
858 objective_position(settings.objective_position_mm)
860 # beam shift
861 beam_shift(settings.beam_shift_um)
863 # apply patterning settings
864 create_pattern(pattern=settings.pattern)
866 return True
869def execute_patterning() -> bool:
870 """
871 Execute the laser patterning.
873 This function starts the laser patterning process.
875 ## Returns
877 bool
878 True if the patterning process is started successfully.
880 """
881 tfs_laser.Patterning_Start()
883 return True
886### main methods
889def mill_region(
890 settings: tbt.LaserSettings,
891) -> bool:
892 """
893 Perform laser milling on a specified region.
895 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.
897 ## Parameters
899 - `settings` (`tbt.LaserSettings`): The laser settings to use for milling.
901 ## Returns
903 - `bool`: True if the milling process is completed successfully.
905 ## Raises
907 - `SystemError`: If the laser is not connected.
908 """
909 # check connection
910 if not laser_connected(): 910 ↛ 911line 910 didn't jump to line 911 because the condition on line 910 was never true
911 raise SystemError("Laser is not connected")
913 microscope = settings.microscope
914 # initial_scan_rotation of ebeam
915 devices.device_access(microscope=microscope)
916 active_beam = factory.active_beam_with_settings(microscope=microscope)
917 scan_settings = factory.active_scan_settings(microscope=microscope)
918 initial_scan_rotation_deg = scan_settings.rotation_deg
920 # apply laser settings
921 apply_laser_settings(
922 image_beam=active_beam,
923 settings=settings,
924 )
926 # insert shutter
927 insert_shutter(microscope=microscope)
929 # execute patterning
930 execute_patterning()
932 # retract shutter
933 retract_shutter(microscope=microscope)
934 time.sleep(1)
936 # reset scan rotation
937 img.beam_scan_rotation(
938 beam=active_beam,
939 microscope=microscope,
940 rotation_deg=initial_scan_rotation_deg,
941 )
943 # TODO: Current code is not very safe here
944 # Below makes sure that the scan reset happens regardless of what happens during milling
945 # try:
946 # apply_laser_settings(
947 # image_beam=active_beam,
948 # settings=settings,
949 # )
950 # insert_shutter(microscope=microscope)
951 # execute_patterning()
952 # finally:
953 # try:
954 # retract_shutter(microscope=microscope)
955 # finally:
956 # img.beam_scan_rotation(
957 # beam=active_beam,
958 # microscope=microscope,
959 # rotation_deg=initial_scan_rotation_deg,
960 # )
962 return True
965def laser_operation(
966 step: tbt.Step, general_settings: tbt.GeneralSettings, slice_number: int
967) -> bool:
968 """
969 Perform a laser operation based on the specified step and settings.
971 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.
973 ## Parameters
975 - `step` (`tbt.Step`): The step object containing the operation settings.
976 - `general_settings` (`tbt.GeneralSettings`): The general settings object.
977 - `slice_number` (`int`): The slice number for the operation.
979 ## Returns
981 - `bool`: True if the laser operation is completed successfully.
982 """
983 # log laser power before
984 laser_power_w = read_power()
985 log.laser_power(
986 step_number=step.number,
987 step_name=step.name,
988 slice_number=slice_number,
989 log_filepath=general_settings.log_filepath,
990 dataset_name=Constants.pre_lasing_dataset_name,
991 power_w=laser_power_w,
992 )
994 mill_region(settings=step.operation_settings)
996 # log laser power after
997 laser_power_w = read_power()
998 log.laser_power(
999 step_number=step.number,
1000 step_name=step.name,
1001 slice_number=slice_number,
1002 log_filepath=general_settings.log_filepath,
1003 dataset_name=Constants.post_lasing_dataset_name,
1004 power_w=laser_power_w,
1005 )
1007 return True
1010def map_ebsd() -> bool:
1011 """
1012 Start an EBSD map and ensure it takes the minimum expected time.
1014 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.
1016 ## Returns
1018 bool
1019 True if the EBSD mapping is completed successfully.
1021 ## Raises
1023 - `ValueError`: If the mapping process does not take the minimum expected time.
1024 """
1025 start_time = time.time()
1026 tfs_laser.EBSD_StartMap()
1027 time.sleep(1)
1028 end_time = time.time()
1029 map_time = end_time - start_time
1030 if map_time < Constants.min_map_time_s:
1031 raise ValueError(
1032 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EBSD mapping software"
1033 )
1034 print(f"\t\tMapping Complete in {int(map_time)} seconds.")
1035 return True
1038def map_eds() -> bool:
1039 """
1040 Start an EDS map and ensure it takes the minimum expected time.
1042 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.
1044 ## Returns
1046 bool
1047 True if the EDS mapping is completed successfully.
1049 ## Raises
1051 - `ValueError`: If the mapping process does not take the minimum expected time.
1052 """
1053 start_time = time.time()
1054 tfs_laser.EDS_StartMap()
1055 time.sleep(1)
1056 end_time = time.time()
1057 map_time = end_time - start_time
1058 if map_time < Constants.min_map_time_s:
1059 raise ValueError(
1060 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EDS mapping software"
1061 )
1062 print(f"\t\tMapping Complete in {int(map_time)} seconds.")
1063 return True