Coverage for src/pytribeam/laser.py: 35%
248 statements
« prev ^ index » next coverage.py v7.6.1, created at 2026-09-03 19:02 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2026-09-03 19:02 +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
167except ImportError:
168 tfs_laser = None
169 print("WARNING: Laser API not imported!")
170 print("\tLaser control, as well as EBSD and EDS control are unavailable.")
171else:
172 print("Laser PythonControl API imported.")
174# 3rd party .whl modules
176# Local scripts
177from pytribeam.constants import Constants
178import pytribeam.factory as factory
179import pytribeam.types as tbt
180import pytribeam.utilities as ut
181import pytribeam.insertable_devices as devices
182import pytribeam.image as img
183import pytribeam.log as log
186def laser_state_to_db(state: tbt.LaserState) -> dict:
187 """
188 This function converts a `LaserState` object into a flattened dictionary representation.
190 ## Parameters
192 - `state` (`tbt.LaserState`): The laser state object to convert.
194 ## Returns
196 - `dict`: A flattened dictionary representation of the laser state.
197 """
198 db = {}
200 db["wavelength_nm"] = state.wavelength_nm
201 db["frequency_khz"] = state.frequency_khz
202 db["pulse_divider"] = state.pulse_divider
203 db["pulse_energy_uj"] = state.pulse_energy_uj
204 db["objective_position_mm"] = state.objective_position_mm
205 db["expected_pattern_duration_s"] = state.expected_pattern_duration_s
207 beam_shift = state.beam_shift_um
208 db["beam_shift_um_x"] = beam_shift.x
209 db["beam_shift_um_y"] = beam_shift.y
211 # we can name these differently depending on the needs of the GUI
212 pattern = state.pattern
213 db["laser_pattern_mode"] = pattern.mode.value
214 db["laser_pattern_rotation_deg"] = pattern.rotation_deg
215 db["laser_pattern_pulses_per_pixel"] = pattern.pulses_per_pixel
216 db["laser_pattern_pixel_dwell_ms"] = pattern.pixel_dwell_ms
218 geometry = pattern.geometry
219 db["passes"] = geometry.passes
220 db["laser_scan_type"] = geometry.scan_type.value
221 db["geometry_type"] = geometry.type.value
223 if geometry.type == tbt.LaserPatternType.BOX:
224 db["size_x_um"] = geometry.size_x_um
225 db["size_y_um"] = geometry.size_y_um
226 db["pitch_x_um"] = geometry.pitch_x_um
227 db["pitch_y_um"] = geometry.pitch_y_um
228 db["coordinate_ref"] = geometry.coordinate_ref
230 if geometry.type == tbt.LaserPatternType.LINE: 230 ↛ 231, 230 ↛ 2342 missed branches: 1) line 230 didn't jump to line 231 because the condition on line 230 was never true, 2) line 230 didn't jump to line 234 because the condition on line 230 was always true
231 db["size_um"] = geometry.size_um
232 db["pitch_um"] = geometry.pitch_um
234 return db
237def laser_connected() -> bool:
238 """
239 Check if the laser is connected.
241 This function tests the connection to the laser and returns True if the connection is successful.
243 ## Returns
245 bool
246 True if the laser is connected, False otherwise.
248 """
249 connect_msg = "Connection test successful.\n"
250 laser_status = io.StringIO()
251 try:
252 with contextlib.redirect_stdout(laser_status):
253 tfs_laser.TestConnection()
254 except:
255 return False
256 else:
257 if laser_status.getvalue() == connect_msg:
258 return True
259 return False
262def _device_connections() -> tbt.DeviceStatus:
263 """
264 Check the connection status of the laser and associated external devices.
266 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.
268 ## Returns
270 tbt.DeviceStatus
271 The connection status of the laser, EBSD, and EDS devices.
273 """
274 # laser must be connected to connect with other devices:
275 if not laser_connected(): 275 ↛ 280line 275 didn't jump to line 280 because the condition on line 275 was always true
276 laser = tbt.RetractableDeviceState.ERROR
277 ebsd = tbt.RetractableDeviceState.ERROR
278 eds = tbt.RetractableDeviceState.ERROR
279 else:
280 laser = tbt.RetractableDeviceState.CONNECTED
281 ebsd = devices.connect_EBSD() # retractable device state
282 eds = devices.connect_EDS() # retractable device state
284 return tbt.DeviceStatus(
285 laser=laser,
286 ebsd=ebsd,
287 eds=eds,
288 )
291def pattern_mode(mode: tbt.LaserPatternMode) -> bool:
292 """
293 Set the laser pattern mode.
295 This function sets the laser pattern mode and verifies that it has been set correctly.
297 ## Parameters
299 - `mode` (`tbt.LaserPatternMode`): The laser pattern mode to set.
301 ## Returns
303 - `bool`: True if the pattern mode is set correctly.
305 ## Raises
307 - `SystemError`: If the pattern mode cannot be set correctly.
308 """
309 tfs_laser.Patterning_Mode(mode.value)
310 laser_state = factory.active_laser_state()
311 if laser_state.pattern.mode != mode: 311 ↛ 313line 311 didn't jump to line 313 because the condition on line 311 was always true
312 raise SystemError("Unable to correctly set pattern mode.")
313 return True
316def pulse_energy_uj(
317 energy_uj: float,
318 energy_tol_uj: float = Constants.laser_energy_tol_uj,
319 delay_s: float = 3.0,
320) -> bool:
321 """
322 Set the pulse energy on the laser.
324 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.
326 ## Parameters
328 - `energy_uj` (`float`): The pulse energy to set in microjoules.
329 - `energy_tol_uj` (`float, optional`): The tolerance for the pulse energy in microjoules (default is Constants.laser_energy_tol_uj).
330 - `delay_s` (`float, optional`): The delay in seconds after setting the pulse energy (default is 3.0 seconds).
332 ## Returns
334 - `bool`: True if the pulse energy is set correctly.
336 ## Raises
338 - `ValueError`: If the pulse energy cannot be set correctly.
339 """
340 tfs_laser.Laser_SetPulseEnergy_MicroJoules(energy_uj)
341 time.sleep(delay_s)
342 laser_state = factory.active_laser_state()
343 if not ut.in_interval( 343 ↛ 351, 343 ↛ 3552 missed branches: 1) line 343 didn't jump to line 351 because the condition on line 343 was never true, 2) line 343 didn't jump to line 355 because the condition on line 343 was always true
344 val=laser_state.pulse_energy_uj,
345 limit=tbt.Limit(
346 min=energy_uj - energy_tol_uj,
347 max=energy_uj + energy_tol_uj,
348 ),
349 type=tbt.IntervalType.CLOSED,
350 ):
351 raise ValueError(
352 f"Could not properly set pulse energy, requested '{energy_uj}' uJ",
353 f"Current settings is {round(laser_state.pulse_energy_uj, 3)} uJ",
354 )
355 return True
358def pulse_divider(
359 divider: int,
360 delay_s: float = Constants.laser_delay_s,
361) -> bool:
362 """
363 Set the pulse divider on the laser.
365 This function sets the pulse divider on the laser and verifies that it has been set correctly.
367 ## Parameters
369 - `divider` (`int`): The pulse divider to set.
370 - `delay_s` (`float, optional`): The delay in seconds after setting the pulse divider (default is Constants.laser_delay_s).
372 ## Returns
374 - `bool`: True if the pulse divider is set correctly.
376 ## Raises
378 - `ValueError`: If the pulse divider cannot be set correctly.
379 """
380 tfs_laser.Laser_PulseDivider(divider)
381 time.sleep(delay_s)
382 laser_state = factory.active_laser_state()
383 if laser_state.pulse_divider != divider:
384 raise ValueError(
385 f"Could not properly set pulse divider, requested '{divider}'",
386 f"Current settings have a divider of {laser_state.pulse_divider}.",
387 )
388 return True
391def set_wavelength(
392 wavelength: tbt.LaserWavelength,
393 frequency_khz: float = 60, # make constnat
394 timeout_s: int = 20, # 120, # make constant
395 num_attempts: int = 2, # TODO make a constant
396 delay_s: int = 5, # make a constant
397) -> bool:
398 """
399 Set the wavelength and frequency of the laser.
401 This function sets the wavelength and frequency of the laser and verifies that they have been set correctly.
403 ## Parameters
405 - `wavelength` (`tbt.LaserWavelength`): The wavelength to set.
406 - `frequency_khz` (`float, optional`): The frequency to set in kHz (default is 60 kHz).
407 - `timeout_s` (`int, optional`): The timeout in seconds for each attempt (default is 20 seconds).
408 - `num_attempts` (`int, optional`): The number of attempts to set the wavelength and frequency (default is 2).
409 - `delay_s` (`int, optional`): The delay in seconds between checks (default is 5 seconds).
411 ## Returns
413 - `bool`: True if the wavelength and frequency are set correctly, False otherwise.
414 """
416 def correct_preset(laser_state: tbt.LaserState):
417 if laser_state.wavelength_nm == wavelength:
418 return math.isclose(laser_state.frequency_khz, frequency_khz, rel_tol=0.05)
419 # TODO use constant for tolerance):
420 return False
422 for _ in range(num_attempts): 422 ↛ 441line 422 didn't jump to line 441 because the loop on line 422 didn't complete
423 if correct_preset(factory.active_laser_state()): 423 ↛ 425line 423 didn't jump to line 425 because the condition on line 423 was always true
424 return True
425 print("Adjusting preset...")
426 tfs_laser.Laser_SetPreset(
427 wavelength_nm=wavelength.value, frequency_kHz=frequency_khz
428 )
429 time_remaining = timeout_s
430 while time_remaining > 0: 430 ↛ 422line 430 didn't jump to line 422 because the condition on line 430 was always true
431 laser_state = factory.active_laser_state()
432 # print(time_remaining, laser_state.frequency_khz)
433 if correct_preset(laser_state=laser_state):
434 return True
435 time.sleep(delay_s)
436 time_remaining -= delay_s
438 # TODO: This does not verify that the wavelength was set and does not match the other functions here
439 # Perhaps this should be modified to raise an error if it does not set?
440 # Might be a lase API thing though?
441 return False
444def read_power(delay_s: float = Constants.laser_delay_s) -> float:
445 """
446 Measure the laser power in watts.
448 This function measures the laser power using an external power meter.
450 ## Parameters
452 - `delay_s` (`float, optional`): The delay in seconds before reading the power (default is Constants.laser_delay_s).
454 ## Returns
456 - `float`: The measured laser power in watts.
457 """
458 # TODO: Perhaps a try/finally structure would be safer here?
459 # Unless the laser API ensures that emission is off if it fails?
460 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON()
461 # try:
462 # tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset()
463 # tfs_laser.Laser_FireContinuously_Start()
464 # try:
465 # time.sleep(delay_s)
466 # return tfs_laser.Laser_ExternalPowerMeter_ReadPower()
467 # finally:
468 # tfs_laser.Laser_FireContinuously_Stop()
469 # finally:
470 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF()
471 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON()
472 tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset()
473 tfs_laser.Laser_FireContinuously_Start()
474 time.sleep(delay_s)
475 power = tfs_laser.Laser_ExternalPowerMeter_ReadPower()
476 tfs_laser.Laser_FireContinuously_Stop()
477 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF()
478 return power
481def insert_shutter(microscope: tbt.Microscope) -> bool:
482 """
483 Insert the laser shutter.
485 This function inserts the laser shutter and verifies that it has been inserted correctly.
487 ## Parameters
489 - `microscope` (`tbt.Microscope`): The microscope object for which to insert the laser shutter.
491 ## Returns
493 - `bool`: True if the laser shutter is successfully inserted.
495 ## Raises
497 - `SystemError`: If the laser shutter cannot be inserted.
498 """
499 devices.CCD_view(microscope=microscope)
500 if tfs_laser.Shutter_GetState() != "Inserted": 500 ↛ 502line 500 didn't jump to line 502 because the condition on line 500 was always true
501 tfs_laser.Shutter_Insert()
502 state = tfs_laser.Shutter_GetState()
503 if state != "Inserted": 503 ↛ 507line 503 didn't jump to line 507 because the condition on line 503 was always true
504 raise SystemError(
505 f"Could not insert laser shutter, current laser shutter state is '{state}'."
506 )
507 devices.CCD_pause(microscope=microscope)
508 return True
511def retract_shutter(microscope: tbt.Microscope) -> bool:
512 """
513 Retract the laser shutter.
515 This function retracts the laser shutter and verifies that it has been retracted correctly.
517 ## Parameters
519 - `microscope` (`tbt.Microscope`): The microscope object for which to retract the laser shutter.
521 ## Returns
523 - `bool`: True if the laser shutter is successfully retracted.
525 ## Raises
527 - `SystemError`: If the laser shutter cannot be retracted.
528 """
529 devices.CCD_view(microscope=microscope)
530 if tfs_laser.Shutter_GetState() != "Retracted":
531 tfs_laser.Shutter_Retract()
532 state = tfs_laser.Shutter_GetState()
533 if state != "Retracted": 533 ↛ 537line 533 didn't jump to line 537 because the condition on line 533 was always true
534 raise SystemError(
535 f"Could not retract laser shutter, current laser shutter state is '{state}'."
536 )
537 devices.CCD_pause(microscope=microscope)
538 return True
541def pulse_polarization(
542 polarization: tbt.LaserPolarization, wavelength: tbt.LaserWavelength
543) -> bool:
544 """
545 Configure the polarization of the laser light.
547 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:
548 - Waveplate_None switches to Vert. (P)
549 - Waveplate_1030 switches to Horiz. (S)
550 - Waveplate_515 switches to Horiz. (S)
552 ## Parameters
554 - `polarization` (`tbt.LaserPolarization`): The desired polarization of the laser light.
555 - `wavelength` (`tbt.LaserWavelength`): The wavelength of the laser light.
557 ## Returns
559 - `bool`: True if the polarization is set correctly.
561 ## Raises
563 - `KeyError`: If the laser wavelength or pulse polarization is invalid.
564 """
565 if polarization == tbt.LaserPolarization.VERTICAL:
566 tfs_laser.FlipperConfiguration("Waveplate_None")
567 return True
568 elif polarization == tbt.LaserPolarization.HORIZONTAL:
569 match_db = {
570 tbt.LaserWavelength.NM_1030: "Waveplate_1030",
571 tbt.LaserWavelength.NM_515: "Waveplate_515",
572 }
573 try:
574 tfs_laser.FlipperConfiguration(match_db[wavelength])
575 except KeyError:
576 raise KeyError(
577 f"Invalid laser wavelength, valid options are {[i.value for i in tbt.LaserWavelength]}"
578 )
579 return True
580 else:
581 raise KeyError(
582 f"Invalid pulse polarization, valid options are {[i.value for i in tbt.LaserPolarization]}"
583 )
586def pulse_settings(pulse: tbt.LaserPulse) -> bool:
587 """
588 Apply the pulse settings to the laser.
590 This function applies the specified pulse settings to the laser, including wavelength, pulse divider, pulse energy, and polarization.
592 ## Parameters
594 - `pulse` (`tbt.LaserPulse`): The pulse settings to apply.
596 ## Returns
598 - `bool`: True if the pulse settings are applied correctly.
599 """
600 active_state = factory.active_laser_state()
601 if pulse.wavelength_nm != active_state.wavelength_nm:
602 # wavelength settings
603 set_wavelength(wavelength=pulse.wavelength_nm)
604 pulse_divider(divider=pulse.divider)
605 pulse_energy_uj(energy_uj=pulse.energy_uj)
606 pulse_polarization(polarization=pulse.polarization, wavelength=pulse.wavelength_nm)
607 return True
610def retract_laser_objective() -> bool:
611 """
612 Retract the laser objective to a safe position.
614 This function retracts the laser objective to a predefined safe position.
616 ## Returns
618 bool
619 True if the laser objective is successfully retracted.
621 """
622 objective_position(position_mm=Constants.laser_objective_retracted_mm)
623 return True
626def objective_position(
627 position_mm: float,
628 tolerance_mm=Constants.laser_objective_tolerance_mm,
629) -> bool:
630 """
631 Move the laser objective to the requested position.
633 This function moves the laser objective to the specified position and verifies that it has been moved correctly.
635 ## Parameters
637 - `position_mm` (`float`): The desired position of the laser objective in millimeters.
638 - `tolerance_mm` (`float, optional`): The tolerance for the laser objective position in millimeters (default is Constants.laser_objective_tolerance_mm).
640 ## Returns
642 - `bool`: True if the laser objective is moved to the requested position correctly.
644 ## Raises
646 - `ValueError`: If the requested position is out of range.
647 - `SystemError`: If the laser objective cannot be moved to the requested position.
648 """
649 tfs_laser.LIP_UnlockZ()
651 if not ut.in_interval( 651 ↛ 656, 651 ↛ 6602 missed branches: 1) line 651 didn't jump to line 656 because the condition on line 651 was never true, 2) line 651 didn't jump to line 660 because the condition on line 651 was always true
652 val=position_mm,
653 limit=Constants.laser_objective_limit_mm,
654 type=tbt.IntervalType.CLOSED,
655 ):
656 raise ValueError(
657 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."
658 )
660 for _ in range(2): 660 ↛ 671line 660 didn't jump to line 671 because the loop on line 660 didn't complete
661 if ut.in_interval( 661 ↛ 668, 661 ↛ 6692 missed branches: 1) line 661 didn't jump to line 668 because the condition on line 661 was never true, 2) line 661 didn't jump to line 669 because the condition on line 661 was always true
662 val=tfs_laser.LIP_GetZPosition(),
663 limit=tbt.Limit(
664 min=position_mm - tolerance_mm, max=position_mm + tolerance_mm
665 ),
666 type=tbt.IntervalType.CLOSED,
667 ):
668 return True
669 tfs_laser.LIP_SetZPosition(position_mm, asynchronously=False)
671 raise SystemError(
672 f"Unable to move laser injection port objective to requested position of {position_mm} +/- {tolerance_mm} mm.",
673 f"Currently at {tfs_laser.LIP_GetZPosition()} mm.",
674 )
677def _shift_axis(
678 target: float,
679 current: float,
680 tolerance: float,
681 axis: str,
682) -> bool:
683 """
684 Helper function for beam shift.
686 This function adjusts the beam shift for the specified axis to the target value within the given tolerance.
688 ## Parameters
690 - `target` (`float`): The target value for the beam shift.
691 - `current` (`float`): The current value of the beam shift.
692 - `tolerance` (`float`): The tolerance for the beam shift.
693 - `axis` (`str`): The axis to adjust ("X" or "Y").
695 ## Returns
697 - `bool`: True if the beam shift is adjusted to the target value correctly, False otherwise.
698 """
699 for _ in range(2): 699 ↛ 716line 699 didn't jump to line 716 because the loop on line 699 didn't complete
700 if ut.in_interval( 700 ↛ 708, 700 ↛ 7092 missed branches: 1) line 700 didn't jump to line 708 because the condition on line 700 was never true, 2) line 700 didn't jump to line 709 because the condition on line 700 was always true
701 val=current,
702 limit=tbt.Limit(
703 min=target - tolerance,
704 max=target + tolerance,
705 ),
706 type=tbt.IntervalType.CLOSED,
707 ):
708 return True
709 if axis == "X": 709 ↛ 710, 709 ↛ 7122 missed branches: 1) line 709 didn't jump to line 710 because the condition on line 709 was never true, 2) line 709 didn't jump to line 712 because the condition on line 709 was always true
710 tfs_laser.BeamShift_Set_X(value=target)
711 current = tfs_laser.BeamShift_Get_X()
712 if axis == "Y":
713 tfs_laser.BeamShift_Set_Y(value=target)
714 current = tfs_laser.BeamShift_Get_Y()
716 return False
719def beam_shift(
720 shift_um: tbt.Point,
721 shift_tolerance_um: float = Constants.laser_beam_shift_tolerance_um,
722) -> bool:
723 """
724 Adjust the laser beam shift to the specified values.
726 This function adjusts the laser beam shift to the specified x and y values within the given tolerance.
728 ## Parameters
730 - `shift_um` (`tbt.Point`): The target beam shift values in micrometers.
731 - `shift_tolerance_um` (`float, optional`): The tolerance for the beam shift in micrometers (default is Constants.laser_beam_shift_tolerance_um).
733 ## Returns
735 - `bool`: True if the beam shift is adjusted to the target values correctly.
737 ## Raises
739 - `ValueError`: If the beam shift cannot be adjusted to the target values.
740 """
741 current_shift_x = tfs_laser.BeamShift_Get_X()
742 current_shift_y = tfs_laser.BeamShift_Get_Y()
744 if not ( 744 ↛ 761line 744 didn't jump to line 761 because the condition on line 744 was always true
745 _shift_axis(
746 target=shift_um.x,
747 current=current_shift_x,
748 tolerance=shift_tolerance_um,
749 axis="X",
750 )
751 and _shift_axis(
752 target=shift_um.y,
753 current=current_shift_y,
754 tolerance=shift_tolerance_um,
755 axis="Y",
756 )
757 ):
758 raise ValueError(
759 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)."
760 )
761 return True
764def create_pattern(pattern: tbt.LaserPattern) -> bool:
765 """
766 Create a laser pattern and check that it is set correctly.
768 This function creates a laser pattern based on the specified pattern settings and verifies that it has been set correctly.
770 ## Parameters
772 - `pattern` (`tbt.LaserPattern`): The laser pattern settings to create.
774 ## Returns
776 - `bool`: True if the pattern is created and set correctly.
778 ## Raises
780 - `ValueError`: If the pattern geometry type is unsupported.
781 - `SystemError`: If the pattern cannot be set correctly.
782 """
783 pattern_mode(mode=pattern.mode)
785 # check if pattern is empty or not
786 if isinstance(pattern.geometry, tbt.LaserBoxPattern):
787 box = pattern.geometry
788 tfs_laser.Patterning_CreatePattern_Box(
789 sizeX_um=box.size_x_um,
790 sizeY_um=box.size_y_um,
791 pitchX_um=box.pitch_x_um,
792 pitchY_um=box.pitch_y_um,
793 dwellTime_ms=pattern.pixel_dwell_ms,
794 passes_int=box.passes,
795 pulsesPerPixel_int=pattern.pulses_per_pixel,
796 scanrotation_degrees=pattern.rotation_deg,
797 scantype_string=box.scan_type.value, # cast enum to string
798 coordinateReference_string=box.coordinate_ref.value, # cast enum to string
799 )
800 elif isinstance(pattern.geometry, tbt.LaserLinePattern): 800 ↛ 801, 800 ↛ 8122 missed branches: 1) line 800 didn't jump to line 801 because the condition on line 800 was never true, 2) line 800 didn't jump to line 812 because the condition on line 800 was always true
801 line = pattern.geometry
802 tfs_laser.Patterning_CreatePattern_Line(
803 sizeX_um=line.size_um,
804 pitchX_um=line.pitch_um,
805 dwellTime_ms=pattern.pixel_dwell_ms,
806 passes_int=line.passes,
807 pulsesPerPixel_int=pattern.pulses_per_pixel,
808 scanrotation_degrees=pattern.rotation_deg,
809 scantype_string=line.scan_type.value, # cast enum to string
810 )
811 else:
812 raise ValueError(
813 f"Unsupported pattern geometry of type '{type(pattern.geometry)}'. Supported types are {tbt.LaserLinePattern, tbt.LaserBoxPattern}"
814 )
815 laser_state = factory.active_laser_state()
816 if laser_state.pattern != pattern:
817 raise SystemError("Unable to correctly set Pattern.")
818 return True
821def apply_laser_settings(image_beam: tbt.Beam, settings: tbt.LaserSettings) -> bool:
822 """
823 Apply the laser settings to the current patterning.
825 This function applies the specified laser settings to the current patterning, including beam scan rotation, pulse settings, objective position, beam shift, and patterning settings.
827 ## Parameters
829 - `image_beam` (`tbt.Beam`): The beam settings for the image.
830 - `settings` (`tbt.LaserSettings`): The laser settings to apply.
832 ## Returns
834 - `bool`: True if the laser settings are applied correctly.
835 """
836 microscope = settings.microscope
838 # forces rotation of electron beam for laser (TFS required)
839 img.beam_scan_rotation(
840 beam=image_beam,
841 microscope=microscope,
842 rotation_deg=Constants.image_scan_rotation_for_laser_deg,
843 )
844 # pulse settings
845 pulse_settings(pulse=settings.pulse)
847 # objective position
848 objective_position(settings.objective_position_mm)
850 # beam shift
851 beam_shift(settings.beam_shift_um)
853 # apply patterning settings
854 create_pattern(pattern=settings.pattern)
856 return True
859def execute_patterning() -> bool:
860 """
861 Execute the laser patterning.
863 This function starts the laser patterning process.
865 ## Returns
867 bool
868 True if the patterning process is started successfully.
870 """
871 tfs_laser.Patterning_Start()
873 return True
876### main methods
879def mill_region(
880 settings: tbt.LaserSettings,
881) -> bool:
882 """
883 Perform laser milling on a specified region.
885 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.
887 ## Parameters
889 - `settings` (`tbt.LaserSettings`): The laser settings to use for milling.
891 ## Returns
893 - `bool`: True if the milling process is completed successfully.
895 ## Raises
897 - `SystemError`: If the laser is not connected.
898 """
899 # check connection
900 if not laser_connected():
901 raise SystemError("Laser is not connected")
903 microscope = settings.microscope
904 # initial_scan_rotation of ebeam
905 devices.device_access(microscope=microscope)
906 active_beam = factory.active_beam_with_settings(microscope=microscope)
907 scan_settings = factory.active_scan_settings(microscope=microscope)
908 initial_scan_rotation_deg = scan_settings.rotation_deg
910 # apply laser settings
911 apply_laser_settings(
912 image_beam=active_beam,
913 settings=settings,
914 )
916 # insert shutter
917 insert_shutter(microscope=microscope)
919 # execute patterning
920 execute_patterning()
922 # retract shutter
923 retract_shutter(microscope=microscope)
924 time.sleep(1)
926 # reset scan rotation
927 img.beam_scan_rotation(
928 beam=active_beam,
929 microscope=microscope,
930 rotation_deg=initial_scan_rotation_deg,
931 )
933 # TODO: Current code is not very safe here
934 # Below makes sure that the scan reset happens regardless of what happens during milling
935 # try:
936 # apply_laser_settings(
937 # image_beam=active_beam,
938 # settings=settings,
939 # )
940 # insert_shutter(microscope=microscope)
941 # execute_patterning()
942 # finally:
943 # try:
944 # retract_shutter(microscope=microscope)
945 # finally:
946 # img.beam_scan_rotation(
947 # beam=active_beam,
948 # microscope=microscope,
949 # rotation_deg=initial_scan_rotation_deg,
950 # )
952 return True
955def laser_operation(
956 step: tbt.Step, general_settings: tbt.GeneralSettings, slice_number: int
957) -> bool:
958 """
959 Perform a laser operation based on the specified step and settings.
961 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.
963 ## Parameters
965 - `step` (`tbt.Step`): The step object containing the operation settings.
966 - `general_settings` (`tbt.GeneralSettings`): The general settings object.
967 - `slice_number` (`int`): The slice number for the operation.
969 ## Returns
971 - `bool`: True if the laser operation is completed successfully.
972 """
973 # log laser power before
974 laser_power_w = read_power()
975 log.laser_power(
976 step_number=step.number,
977 step_name=step.name,
978 slice_number=slice_number,
979 log_filepath=general_settings.log_filepath,
980 dataset_name=Constants.pre_lasing_dataset_name,
981 power_w=laser_power_w,
982 )
984 mill_region(settings=step.operation_settings)
986 # log laser power after
987 laser_power_w = read_power()
988 log.laser_power(
989 step_number=step.number,
990 step_name=step.name,
991 slice_number=slice_number,
992 log_filepath=general_settings.log_filepath,
993 dataset_name=Constants.post_lasing_dataset_name,
994 power_w=laser_power_w,
995 )
997 return True
1000def map_ebsd() -> bool:
1001 """
1002 Start an EBSD map and ensure it takes the minimum expected time.
1004 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.
1006 ## Returns
1008 bool
1009 True if the EBSD mapping is completed successfully.
1011 ## Raises
1013 - `ValueError`: If the mapping process does not take the minimum expected time.
1014 """
1015 start_time = time.time()
1016 tfs_laser.EBSD_StartMap()
1017 time.sleep(1)
1018 end_time = time.time()
1019 map_time = end_time - start_time
1020 if map_time < Constants.min_map_time_s:
1021 raise ValueError(
1022 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EBSD mapping software"
1023 )
1024 print(f"\t\tMapping Complete in {int(map_time)} seconds.")
1025 return True
1028def map_eds() -> bool:
1029 """
1030 Start an EDS map and ensure it takes the minimum expected time.
1032 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.
1034 ## Returns
1036 bool
1037 True if the EDS mapping is completed successfully.
1039 ## Raises
1041 - `ValueError`: If the mapping process does not take the minimum expected time.
1042 """
1043 start_time = time.time()
1044 tfs_laser.EDS_StartMap()
1045 time.sleep(1)
1046 end_time = time.time()
1047 map_time = end_time - start_time
1048 if map_time < Constants.min_map_time_s:
1049 raise ValueError(
1050 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EDS mapping software"
1051 )
1052 print(f"\t\tMapping Complete in {int(map_time)} seconds.")
1053 return True