Coverage for src\pytribeam\laser.py: 71%
250 statements
« prev ^ index » next coverage.py v7.5.1, created at 2025-03-04 17:41 -0800
« prev ^ index » next coverage.py v7.5.1, created at 2025-03-04 17:41 -0800
1#!/usr/bin/python3
3# Default python modules
4import os
5from pathlib import Path
6import time
7import warnings
8import contextlib, io
9import math
11try:
12 import Laser.PythonControl as tfs_laser
14 print("Laser PythonControl API imported.")
15except:
16 print("WARNING: Laser API not imported!")
17 print("\tLaser control, as well as EBSD and EDS control are unavailable.")
19# 3rd party .whl modules
20import h5py
22# Local scripts
23from pytribeam.constants import Conversions, Constants
24import pytribeam.factory as factory
25import pytribeam.types as tbt
26import pytribeam.utilities as ut
27import pytribeam.insertable_devices as devices
28import pytribeam.image as img
29import pytribeam.log as log
32def laser_state_to_db(state: tbt.LaserState) -> dict:
33 """converts a laser state type into a flattened dictionary"""
34 db = {}
36 db["wavelength_nm"] = state.wavelength_nm
37 db["frequency_khz"] = state.frequency_khz
38 db["pulse_divider"] = state.pulse_divider
39 db["pulse_energy_uj"] = state.pulse_energy_uj
40 db["objective_position_mm"] = state.objective_position_mm
41 db["expected_pattern_duration_s"] = state.expected_pattern_duration_s
43 beam_shift = state.beam_shift_um
44 db["beam_shift_um_x"] = beam_shift.x
45 db["beam_shift_um_y"] = beam_shift.y
47 # we can name these differently depending on the needs of the GUI
48 pattern = state.pattern
49 db["laser_pattern_mode"] = pattern.mode.value
50 db["laser_pattern_rotation_deg"] = pattern.rotation_deg
51 db["laser_pattern_pulses_per_pixel"] = pattern.pulses_per_pixel
52 db["laser_pattern_pixel_dwell_ms"] = pattern.pixel_dwell_ms
54 geometry = pattern.geometry
55 db["passes"] = geometry.passes
56 db["laser_scan_type"] = geometry.scan_type.value
57 db["geometry_type"] = geometry.type.value
59 if geometry.type == tbt.LaserPatternType.BOX:
60 db["size_x_um"] = geometry.size_x_um
61 db["size_y_um"] = geometry.size_y_um
62 db["pitch_x_um"] = geometry.pitch_x_um
63 db["pitch_y_um"] = geometry.pitch_y_um
64 db["coordinate_ref"] = geometry.coordinate_ref
66 if geometry.type == tbt.LaserPatternType.LINE:
67 db["size_um"] = geometry.size_um
68 db["pitch_um"] = geometry.pitch_um
70 return db
73def laser_connected() -> bool:
74 connect_msg = "Connection test successful.\n"
75 laser_status = io.StringIO()
76 try:
77 with contextlib.redirect_stdout(laser_status):
78 tfs_laser.TestConnection()
79 except:
80 return False
81 else:
82 if laser_status.getvalue() == connect_msg:
83 return True
84 return False
87def _device_connections() -> tbt.DeviceStatus:
88 """checks laser connection and associated external devices. Meant only to be a quick tool for the GUI, as it will not provide the user with additional info to try and fix it."""
89 # laser must be connected to connect with other devices:
90 if not laser_connected():
91 laser = tbt.RetractableDeviceState.ERROR
92 ebsd = tbt.RetractableDeviceState.ERROR
93 eds = tbt.RetractableDeviceState.ERROR
94 else:
95 laser = tbt.RetractableDeviceState.CONNECTED
96 ebsd = devices.connect_EBSD() # retractable device state
97 eds = devices.connect_EDS() # retractable device state
99 return tbt.DeviceStatus(
100 laser=laser,
101 ebsd=ebsd,
102 eds=eds,
103 )
106def pattern_mode(mode: tbt.LaserPatternMode) -> bool:
107 """Sets pattern mode."""
108 tfs_laser.Patterning_Mode(mode.value)
109 laser_state = factory.active_laser_state()
110 if laser_state.pattern.mode != mode:
111 raise SystemError("Unable to correctly set pattern mode.")
112 return True
115def pulse_energy_uj(
116 energy_uj: float,
117 energy_tol_uj: float = Constants.laser_energy_tol_uj,
118 delay_s: float = 3.0,
119) -> bool:
120 """sets pulse energy on laser, should be done after pulse divider"""
121 tfs_laser.Laser_SetPulseEnergy_MicroJoules(energy_uj)
122 time.sleep(delay_s)
123 laser_state = factory.active_laser_state()
124 if not ut.in_interval(
125 val=laser_state.pulse_energy_uj,
126 limit=tbt.Limit(
127 min=energy_uj - energy_tol_uj,
128 max=energy_uj + energy_tol_uj,
129 ),
130 type=tbt.IntervalType.CLOSED,
131 ):
132 raise ValueError(
133 f"Could not properly set pulse energy, requested '{energy_uj}' uJ",
134 f"Current settings is {round(laser_state.pulse_energy_uj,3)} uJ",
135 )
136 return True
139def pulse_divider(
140 divider: int,
141 delay_s: float = Constants.laser_delay_s,
142) -> bool:
143 """sets pulse divider on laser"""
144 tfs_laser.Laser_PulseDivider(divider)
145 time.sleep(delay_s)
146 laser_state = factory.active_laser_state()
147 if laser_state.pulse_divider != divider:
148 raise ValueError(
149 f"Could not properly set pulse divider, requested '{divider}'",
150 f"Current settings have a divider of {laser_state.pulse_divider}.",
151 )
152 return True
155def set_wavelength(
156 wavelength: tbt.LaserWavelength,
157 frequency_khz: float = 60, # make constnat
158 timeout_s: int = 20, # 120, # make constant
159 num_attempts: int = 2, # TODO make a constant
160 delay_s: int = 5, # make a constant
161) -> bool:
162 def correct_preset(laser_state: tbt.LaserState):
163 if laser_state.wavelength_nm == wavelength:
164 return math.isclose(laser_state.frequency_khz, frequency_khz, rel_tol=0.05)
165 # TODO use constant for tolerance):
166 return False
168 for _ in range(num_attempts):
169 if correct_preset(factory.active_laser_state()):
170 return True
171 print("Adjusting preset...")
172 tfs_laser.Laser_SetPreset(
173 wavelength_nm=wavelength.value, frequency_kHz=frequency_khz
174 )
175 time_remaining = timeout_s
176 while time_remaining > 0:
177 laser_state = factory.active_laser_state()
178 # print(time_remaining, laser_state.frequency_khz)
179 if correct_preset(laser_state=laser_state):
180 return True
181 time.sleep(delay_s)
182 time_remaining -= delay_s
184 return False
187def read_power(delay_s: float = Constants.laser_delay_s) -> float:
188 """measures laser power in watts"""
189 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON()
190 tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset()
191 tfs_laser.Laser_FireContinuously_Start()
192 time.sleep(delay_s)
193 power = tfs_laser.Laser_ExternalPowerMeter_ReadPower()
194 tfs_laser.Laser_FireContinuously_Stop()
195 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF()
196 return power
199def insert_shutter(microscope: tbt.Microscope) -> bool:
200 """inserts laser shutter"""
201 devices.CCD_view(microscope=microscope)
202 if tfs_laser.Shutter_GetState() != "Inserted":
203 tfs_laser.Shutter_Insert()
204 state = tfs_laser.Shutter_GetState()
205 if state != "Inserted":
206 raise SystemError(
207 f"Could not insert laser shutter, current laser shutter state is '{state}'."
208 )
209 devices.CCD_pause(microscope=microscope)
210 return True
213def retract_shutter(microscope: tbt.Microscope) -> bool:
214 """retract laser shutter"""
215 devices.CCD_view(microscope=microscope)
216 if tfs_laser.Shutter_GetState() != "Retracted":
217 tfs_laser.Shutter_Retract()
218 state = tfs_laser.Shutter_GetState()
219 if state != "Retracted":
220 raise SystemError(
221 f"Could not retract laser shutter, current laser shutter state is '{state}'."
222 )
223 devices.CCD_pause(microscope=microscope)
224 return True
227def pulse_polarization(
228 polarization: tbt.LaserPolarization, wavelength: tbt.LaserWavelength
229) -> bool:
230 """configure polarization of laser light, no way to read current value. This is controlled via "FlipperConfiguration", which takes the following strings:
231 Waveplate_None switches to Vert. (P)
232 Waveplate_1030 switches to Horiz. (S)
233 Waveplate_515 switches to Horiz. (S)"""
234 if polarization == tbt.LaserPolarization.VERTICAL:
235 tfs_laser.FlipperConfiguration("Waveplate_None")
236 return True
237 elif polarization == tbt.LaserPolarization.HORIZONTAL:
238 match_db = {
239 tbt.LaserWavelength.NM_1030: "Waveplate_1030",
240 tbt.LaserWavelength.NM_515: "Waveplate_515",
241 }
242 try:
243 tfs_laser.FlipperConfiguration(match_db[wavelength])
244 except KeyError:
245 raise KeyError(
246 f"Invalid laser wavelength, valid options are {[i.value for i in tbt.LaserWavelength]}"
247 )
248 return True
249 else:
250 raise KeyError(
251 f"Invalid pulse polarization, valid options are {[i.value for i in tbt.LaserPolarization]}"
252 )
255def pulse_settings(pulse: tbt.LaserPulse) -> True:
256 """Applies pulse settings"""
257 active_state = factory.active_laser_state()
258 if pulse.wavelength_nm != active_state.wavelength_nm:
259 # wavelength settings
260 set_wavelength(wavelength=pulse.wavelength_nm)
261 pulse_divider(divider=pulse.divider)
262 pulse_energy_uj(energy_uj=pulse.energy_uj)
263 pulse_polarization(polarization=pulse.polarization, wavelength=pulse.wavelength_nm)
264 return True
267def retract_laser_objective() -> bool:
268 "Retract laser objective to safe position"
269 objective_position(position_mm=Constants.laser_objective_retracted_mm)
270 return True
273def objective_position(
274 position_mm: float,
275 tolerance_mm=Constants.laser_objective_tolerance_mm,
276) -> bool:
277 """Moves laser objective to requested position"""
278 tfs_laser.LIP_UnlockZ()
280 if not ut.in_interval(
281 val=position_mm,
282 limit=Constants.laser_objective_limit_mm,
283 type=tbt.IntervalType.CLOSED,
284 ):
285 raise ValueError(
286 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."
287 )
289 for _ in range(2):
290 if ut.in_interval(
291 val=tfs_laser.LIP_GetZPosition(),
292 limit=tbt.Limit(
293 min=position_mm - tolerance_mm, max=position_mm + tolerance_mm
294 ),
295 type=tbt.IntervalType.CLOSED,
296 ):
297 return True
298 tfs_laser.LIP_SetZPosition(position_mm, asynchronously=False)
300 raise SystemError(
301 f"Unable to move laser injection port objective to requested position of {position_mm} +/- {tolerance_mm} mm.",
302 f"Currently at {tfs_laser.LIP_GetZPosition()} mm.",
303 )
306def _shift_axis(
307 target: float,
308 current: float,
309 tolerance: float,
310 axis: str,
311) -> bool:
312 """helper function for beam_shift"""
313 for _ in range(2):
314 if ut.in_interval(
315 val=current,
316 limit=tbt.Limit(
317 min=target - tolerance,
318 max=target + tolerance,
319 ),
320 type=tbt.IntervalType.CLOSED,
321 ):
322 return True
323 if axis == "X":
324 tfs_laser.BeamShift_Set_X(value=target)
325 current = tfs_laser.BeamShift_Get_X()
326 if axis == "Y":
327 tfs_laser.BeamShift_Set_Y(value=target)
328 current = tfs_laser.BeamShift_Get_Y()
330 return False
333def beam_shift(
334 shift_um: tbt.Point,
335 shift_tolerance_um: float = Constants.laser_beam_shift_tolerance_um,
336) -> bool:
337 current_shift_x = tfs_laser.BeamShift_Get_X()
338 current_shift_y = tfs_laser.BeamShift_Get_Y()
340 if not (
341 _shift_axis(
342 target=shift_um.x,
343 current=current_shift_x,
344 tolerance=shift_tolerance_um,
345 axis="X",
346 )
347 and _shift_axis(
348 target=shift_um.y,
349 current=current_shift_y,
350 tolerance=shift_tolerance_um,
351 axis="Y",
352 )
353 ):
354 raise ValueError(
355 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)."
356 )
357 return True
360def create_pattern(pattern: tbt.LaserPattern):
361 """Create patterning and check that it is set correctly"""
362 pattern_mode(mode=pattern.mode)
364 # check if pattern is empty or not
365 if isinstance(pattern.geometry, tbt.LaserBoxPattern):
366 box = pattern.geometry
367 tfs_laser.Patterning_CreatePattern_Box(
368 sizeX_um=box.size_x_um,
369 sizeY_um=box.size_y_um,
370 pitchX_um=box.pitch_x_um,
371 pitchY_um=box.pitch_y_um,
372 dwellTime_ms=pattern.pixel_dwell_ms,
373 passes_int=box.passes,
374 pulsesPerPixel_int=pattern.pulses_per_pixel,
375 scanrotation_degrees=pattern.rotation_deg,
376 scantype_string=box.scan_type.value, # cast enum to string
377 coordinateReference_string=box.coordinate_ref.value, # cast enum to string
378 )
379 elif isinstance(pattern.geometry, tbt.LaserLinePattern):
380 line = pattern.geometry
381 tfs_laser.Patterning_CreatePattern_Line(
382 sizeX_um=line.size_um,
383 pitchX_um=line.pitch_um,
384 dwellTime_ms=pattern.pixel_dwell_ms,
385 passes_int=line.passes,
386 pulsesPerPixel_int=pattern.pulses_per_pixel,
387 scanrotation_degrees=pattern.rotation_deg,
388 scantype_string=line.scan_type.value, # cast enum to string
389 )
390 else:
391 raise ValueError(
392 f"Unsupported pattern geometry of type '{type(pattern.geometry)}'. Supported types are {tbt.LaserLinePattern, tbt.LaserBoxPattern}"
393 )
394 laser_state = factory.active_laser_state()
395 if laser_state.pattern != pattern:
396 raise SystemError("Unable to correctly set Pattern.")
397 return True
400def apply_laser_settings(image_beam: tbt.Beam, settings: tbt.LaserSettings) -> bool:
401 """Applies laser settings to current patterning"""
402 microscope = settings.microscope
404 # forces rotation of electron beam for laser (TFS required)
405 img.beam_scan_rotation(
406 beam=image_beam,
407 microscope=microscope,
408 rotation_deg=Constants.image_scan_rotation_for_laser_deg,
409 )
410 # pulse settings
411 pulse_settings(pulse=settings.pulse)
413 # objective position
414 objective_position(settings.objective_position_mm)
416 # beam shift
417 beam_shift(settings.beam_shift_um)
419 # apply patterning settings
420 create_pattern(pattern=settings.pattern)
422 return True
425def execute_patterning() -> bool:
426 tfs_laser.Patterning_Start()
428 return True
431### main methods
434def mill_region(
435 settings: tbt.LaserSettings,
436) -> bool:
437 """laser milling main function"""
438 # check connection
439 if not laser_connected():
440 raise SystemError("Laser is not connected")
442 microscope = settings.microscope
443 # initial_scan_rotation of ebeam
444 devices.device_access(microscope=microscope)
445 active_beam = factory.active_beam_with_settings(microscope=microscope)
446 scan_settings = factory.active_scan_settings(microscope=microscope)
447 initial_scan_rotation_deg = scan_settings.rotation_deg
449 # apply laser settings
450 apply_laser_settings(
451 image_beam=active_beam,
452 settings=settings,
453 )
455 # insert shutter
456 insert_shutter(microscope=microscope)
458 # execute patterning
459 execute_patterning()
461 # retract shutter
462 retract_shutter(microscope=microscope)
463 time.sleep(1)
465 # reset scan rotation
466 img.beam_scan_rotation(
467 beam=active_beam,
468 microscope=microscope,
469 rotation_deg=initial_scan_rotation_deg,
470 )
472 return True
475def laser_operation(
476 step: tbt.Step, general_settings: tbt.GeneralSettings, slice_number: int
477) -> bool:
479 # log laser power before
480 laser_power_w = read_power()
481 log.laser_power(
482 step_number=step.number,
483 step_name=step.name,
484 slice_number=slice_number,
485 log_filepath=general_settings.log_filepath,
486 dataset_name=Constants.pre_lasing_dataset_name,
487 power_w=laser_power_w,
488 )
490 mill_region(settings=step.operation_settings)
492 # log laser power after
493 laser_power_w = read_power()
494 log.laser_power(
495 step_number=step.number,
496 step_name=step.name,
497 slice_number=slice_number,
498 log_filepath=general_settings.log_filepath,
499 dataset_name=Constants.post_lasing_dataset_name,
500 power_w=laser_power_w,
501 )
503 return True
506def map_ebsd() -> bool:
507 start_time = time.time()
508 tfs_laser.EBSD_StartMap()
509 time.sleep(1)
510 end_time = time.time()
511 map_time = end_time - start_time
512 if map_time < Constants.min_map_time_s:
513 raise ValueError(
514 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EBSD mapping software"
515 )
516 print(f"\t\tMapping Complete in {int(map_time)} seconds.")
517 return True
520def map_eds() -> bool:
521 start_time = time.time()
522 tfs_laser.EDS_StartMap()
523 time.sleep(1)
524 end_time = time.time()
525 map_time = end_time - start_time
526 if map_time < Constants.min_map_time_s:
527 raise ValueError(
528 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EDS mapping software"
529 )
530 print(f"\t\tMapping Complete in {int(map_time)} seconds.")
531 return True