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

1#!/usr/bin/python3 

2 

3# Default python modules 

4import os 

5from pathlib import Path 

6import time 

7import warnings 

8import contextlib, io 

9import math 

10 

11try: 

12 import Laser.PythonControl as tfs_laser 

13 

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.") 

18 

19# 3rd party .whl modules 

20import h5py 

21 

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 

30 

31 

32def laser_state_to_db(state: tbt.LaserState) -> dict: 

33 """converts a laser state type into a flattened dictionary""" 

34 db = {} 

35 

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 

42 

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 

46 

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 

53 

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 

58 

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 

65 

66 if geometry.type == tbt.LaserPatternType.LINE: 

67 db["size_um"] = geometry.size_um 

68 db["pitch_um"] = geometry.pitch_um 

69 

70 return db 

71 

72 

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 

85 

86 

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 

98 

99 return tbt.DeviceStatus( 

100 laser=laser, 

101 ebsd=ebsd, 

102 eds=eds, 

103 ) 

104 

105 

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 

113 

114 

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 

137 

138 

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 

153 

154 

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 

167 

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 

183 

184 return False 

185 

186 

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 

197 

198 

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 

211 

212 

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 

225 

226 

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 ) 

253 

254 

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 

265 

266 

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 

271 

272 

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() 

279 

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 ) 

288 

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) 

299 

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 ) 

304 

305 

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() 

329 

330 return False 

331 

332 

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() 

339 

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 

358 

359 

360def create_pattern(pattern: tbt.LaserPattern): 

361 """Create patterning and check that it is set correctly""" 

362 pattern_mode(mode=pattern.mode) 

363 

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 

398 

399 

400def apply_laser_settings(image_beam: tbt.Beam, settings: tbt.LaserSettings) -> bool: 

401 """Applies laser settings to current patterning""" 

402 microscope = settings.microscope 

403 

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) 

412 

413 # objective position 

414 objective_position(settings.objective_position_mm) 

415 

416 # beam shift 

417 beam_shift(settings.beam_shift_um) 

418 

419 # apply patterning settings 

420 create_pattern(pattern=settings.pattern) 

421 

422 return True 

423 

424 

425def execute_patterning() -> bool: 

426 tfs_laser.Patterning_Start() 

427 

428 return True 

429 

430 

431### main methods 

432 

433 

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") 

441 

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 

448 

449 # apply laser settings 

450 apply_laser_settings( 

451 image_beam=active_beam, 

452 settings=settings, 

453 ) 

454 

455 # insert shutter 

456 insert_shutter(microscope=microscope) 

457 

458 # execute patterning 

459 execute_patterning() 

460 

461 # retract shutter 

462 retract_shutter(microscope=microscope) 

463 time.sleep(1) 

464 

465 # reset scan rotation 

466 img.beam_scan_rotation( 

467 beam=active_beam, 

468 microscope=microscope, 

469 rotation_deg=initial_scan_rotation_deg, 

470 ) 

471 

472 return True 

473 

474 

475def laser_operation( 

476 step: tbt.Step, general_settings: tbt.GeneralSettings, slice_number: int 

477) -> bool: 

478 

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 ) 

489 

490 mill_region(settings=step.operation_settings) 

491 

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 ) 

502 

503 return True 

504 

505 

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 

518 

519 

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