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

1#!/usr/bin/python3 

2"""Laser, EBSD, and EDS hardware-control utilities. 

3 

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. 

9 

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. 

14 

15## External dependency 

16 

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. 

20 

21Use `laser_connected` to test whether the laser API can communicate with the 

22laser: 

23 

24```python 

25from pytribeam import laser 

26 

27if not laser.laser_connected(): 

28 raise RuntimeError("Laser is not connected.") 

29``` 

30 

31## Typical usage 

32 

33Run a laser milling operation from workflow settings: 

34 

35```python 

36from pytribeam import laser 

37 

38laser.laser_operation( 

39 step=step, 

40 general_settings=general_settings, 

41 slice_number=slice_number, 

42) 

43``` 

44 

45Apply laser settings and mill a configured region directly: 

46 

47```python 

48from pytribeam import laser 

49 

50laser.mill_region(settings=laser_settings) 

51``` 

52 

53Start EBSD or EDS mapping: 

54 

55```python 

56from pytribeam import laser 

57 

58laser.map_ebsd() 

59laser.map_eds() 

60``` 

61 

62## Main entry points 

63 

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. 

76 

77## Laser configuration workflow 

78 

79`mill_region` performs the standard laser milling sequence: 

80 

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. 

89 

90`laser_operation` wraps this sequence with laser-power measurements before and 

91after milling and records those values in the experiment log. 

92 

93## Pattern support 

94 

95`create_pattern` currently supports: 

96 

97| Geometry type | Laser API pattern | 

98| --- | --- | 

99| `tbt.LaserBoxPattern` | Box pattern | 

100| `tbt.LaserLinePattern` | Line pattern | 

101 

102Unsupported laser pattern geometry types raise `ValueError`. 

103 

104## Units 

105 

106Laser settings use explicit field names to indicate units: 

107 

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 | 

119 

120## EBSD and EDS mapping 

121 

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. 

125 

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. 

132 

133<hr style="height: 12px; background-color: #333; border: none;"> 

134""" 

135 

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] 

159 

160# Default python modules 

161import time 

162import contextlib, io 

163import math 

164from typing import Optional 

165 

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

174 

175# 3rd party .whl modules 

176 

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 

185 

186 

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

188 """ 

189 This function converts a `LaserState` object into a flattened dictionary representation. 

190 

191 ## Parameters 

192 

193 - `state` (`tbt.LaserState`): The laser state object to convert. 

194 

195 ## Returns 

196 

197 - `dict`: A flattened dictionary representation of the laser state. 

198 """ 

199 db = {} 

200 

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 

207 

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 

211 

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 

218 

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 

223 

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 

230 

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

232 db["size_um"] = geometry.size_um 

233 db["pitch_um"] = geometry.pitch_um 

234 

235 return db 

236 

237 

238def laser_connected() -> bool: 

239 """ 

240 Check if the laser is connected. 

241 

242 This function tests the connection to the laser and returns True if the connection is successful. 

243 

244 ## Returns 

245 

246 bool 

247 True if the laser is connected, False otherwise. 

248 

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 

261 

262 

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. 

268 

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. 

270 

271 ## Returns 

272 

273 tbt.DeviceStatus 

274 The connection status of the laser, EBSD, and EDS devices. 

275 

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 

286 

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 = {} 

292 

293 return tbt.DeviceStatus( 

294 laser=laser, 

295 ebsd=ebsd, 

296 eds=eds, 

297 extra_devices=states, 

298 ) 

299 

300 

301def pattern_mode(mode: tbt.LaserPatternMode) -> bool: 

302 """ 

303 Set the laser pattern mode. 

304 

305 This function sets the laser pattern mode and verifies that it has been set correctly. 

306 

307 ## Parameters 

308 

309 - `mode` (`tbt.LaserPatternMode`): The laser pattern mode to set. 

310 

311 ## Returns 

312 

313 - `bool`: True if the pattern mode is set correctly. 

314 

315 ## Raises 

316 

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 

324 

325 

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. 

333 

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. 

335 

336 ## Parameters 

337 

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

341 

342 ## Returns 

343 

344 - `bool`: True if the pulse energy is set correctly. 

345 

346 ## Raises 

347 

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 

366 

367 

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. 

374 

375 This function sets the pulse divider on the laser and verifies that it has been set correctly. 

376 

377 ## Parameters 

378 

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

381 

382 ## Returns 

383 

384 - `bool`: True if the pulse divider is set correctly. 

385 

386 ## Raises 

387 

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 

399 

400 

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. 

410 

411 This function sets the wavelength and frequency of the laser and verifies that they have been set correctly. 

412 

413 ## Parameters 

414 

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

420 

421 ## Returns 

422 

423 - `bool`: True if the wavelength and frequency are set correctly, False otherwise. 

424 """ 

425 

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 

431 

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 

447 

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 

452 

453 

454def read_power(delay_s: float = Constants.laser_delay_s) -> float: 

455 """ 

456 Measure the laser power in watts. 

457 

458 This function measures the laser power using an external power meter. 

459 

460 ## Parameters 

461 

462 - `delay_s` (`float, optional`): The delay in seconds before reading the power (default is Constants.laser_delay_s). 

463 

464 ## Returns 

465 

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 

489 

490 

491def insert_shutter(microscope: tbt.Microscope) -> bool: 

492 """ 

493 Insert the laser shutter. 

494 

495 This function inserts the laser shutter and verifies that it has been inserted correctly. 

496 

497 ## Parameters 

498 

499 - `microscope` (`tbt.Microscope`): The microscope object for which to insert the laser shutter. 

500 

501 ## Returns 

502 

503 - `bool`: True if the laser shutter is successfully inserted. 

504 

505 ## Raises 

506 

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 

519 

520 

521def retract_shutter(microscope: tbt.Microscope) -> bool: 

522 """ 

523 Retract the laser shutter. 

524 

525 This function retracts the laser shutter and verifies that it has been retracted correctly. 

526 

527 ## Parameters 

528 

529 - `microscope` (`tbt.Microscope`): The microscope object for which to retract the laser shutter. 

530 

531 ## Returns 

532 

533 - `bool`: True if the laser shutter is successfully retracted. 

534 

535 ## Raises 

536 

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 

549 

550 

551def pulse_polarization( 

552 polarization: tbt.LaserPolarization, wavelength: tbt.LaserWavelength 

553) -> bool: 

554 """ 

555 Configure the polarization of the laser light. 

556 

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) 

561 

562 ## Parameters 

563 

564 - `polarization` (`tbt.LaserPolarization`): The desired polarization of the laser light. 

565 - `wavelength` (`tbt.LaserWavelength`): The wavelength of the laser light. 

566 

567 ## Returns 

568 

569 - `bool`: True if the polarization is set correctly. 

570 

571 ## Raises 

572 

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 ) 

594 

595 

596def pulse_settings(pulse: tbt.LaserPulse) -> bool: 

597 """ 

598 Apply the pulse settings to the laser. 

599 

600 This function applies the specified pulse settings to the laser, including wavelength, pulse divider, pulse energy, and polarization. 

601 

602 ## Parameters 

603 

604 - `pulse` (`tbt.LaserPulse`): The pulse settings to apply. 

605 

606 ## Returns 

607 

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 

618 

619 

620def retract_laser_objective() -> bool: 

621 """ 

622 Retract the laser objective to a safe position. 

623 

624 This function retracts the laser objective to a predefined safe position. 

625 

626 ## Returns 

627 

628 bool 

629 True if the laser objective is successfully retracted. 

630 

631 """ 

632 objective_position(position_mm=Constants.laser_objective_retracted_mm) 

633 return True 

634 

635 

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. 

642 

643 This function moves the laser objective to the specified position and verifies that it has been moved correctly. 

644 

645 ## Parameters 

646 

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

649 

650 ## Returns 

651 

652 - `bool`: True if the laser objective is moved to the requested position correctly. 

653 

654 ## Raises 

655 

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

660 

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 ) 

669 

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) 

680 

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 ) 

685 

686 

687def _shift_axis( 

688 target: float, 

689 current: float, 

690 tolerance: float, 

691 axis: str, 

692) -> bool: 

693 """ 

694 Helper function for beam shift. 

695 

696 This function adjusts the beam shift for the specified axis to the target value within the given tolerance. 

697 

698 ## Parameters 

699 

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

704 

705 ## Returns 

706 

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

725 

726 return False 

727 

728 

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. 

735 

736 This function adjusts the laser beam shift to the specified x and y values within the given tolerance. 

737 

738 ## Parameters 

739 

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

742 

743 ## Returns 

744 

745 - `bool`: True if the beam shift is adjusted to the target values correctly. 

746 

747 ## Raises 

748 

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

753 

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 

772 

773 

774def create_pattern(pattern: tbt.LaserPattern) -> bool: 

775 """ 

776 Create a laser pattern and check that it is set correctly. 

777 

778 This function creates a laser pattern based on the specified pattern settings and verifies that it has been set correctly. 

779 

780 ## Parameters 

781 

782 - `pattern` (`tbt.LaserPattern`): The laser pattern settings to create. 

783 

784 ## Returns 

785 

786 - `bool`: True if the pattern is created and set correctly. 

787 

788 ## Raises 

789 

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) 

794 

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 

829 

830 

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

832 """ 

833 Apply the laser settings to the current patterning. 

834 

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. 

836 

837 ## Parameters 

838 

839 - `image_beam` (`tbt.Beam`): The beam settings for the image. 

840 - `settings` (`tbt.LaserSettings`): The laser settings to apply. 

841 

842 ## Returns 

843 

844 - `bool`: True if the laser settings are applied correctly. 

845 """ 

846 microscope = settings.microscope 

847 

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) 

856 

857 # objective position 

858 objective_position(settings.objective_position_mm) 

859 

860 # beam shift 

861 beam_shift(settings.beam_shift_um) 

862 

863 # apply patterning settings 

864 create_pattern(pattern=settings.pattern) 

865 

866 return True 

867 

868 

869def execute_patterning() -> bool: 

870 """ 

871 Execute the laser patterning. 

872 

873 This function starts the laser patterning process. 

874 

875 ## Returns 

876 

877 bool 

878 True if the patterning process is started successfully. 

879 

880 """ 

881 tfs_laser.Patterning_Start() 

882 

883 return True 

884 

885 

886### main methods 

887 

888 

889def mill_region( 

890 settings: tbt.LaserSettings, 

891) -> bool: 

892 """ 

893 Perform laser milling on a specified region. 

894 

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. 

896 

897 ## Parameters 

898 

899 - `settings` (`tbt.LaserSettings`): The laser settings to use for milling. 

900 

901 ## Returns 

902 

903 - `bool`: True if the milling process is completed successfully. 

904 

905 ## Raises 

906 

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

912 

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 

919 

920 # apply laser settings 

921 apply_laser_settings( 

922 image_beam=active_beam, 

923 settings=settings, 

924 ) 

925 

926 # insert shutter 

927 insert_shutter(microscope=microscope) 

928 

929 # execute patterning 

930 execute_patterning() 

931 

932 # retract shutter 

933 retract_shutter(microscope=microscope) 

934 time.sleep(1) 

935 

936 # reset scan rotation 

937 img.beam_scan_rotation( 

938 beam=active_beam, 

939 microscope=microscope, 

940 rotation_deg=initial_scan_rotation_deg, 

941 ) 

942 

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

961 

962 return True 

963 

964 

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. 

970 

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. 

972 

973 ## Parameters 

974 

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. 

978 

979 ## Returns 

980 

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 ) 

993 

994 mill_region(settings=step.operation_settings) 

995 

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 ) 

1006 

1007 return True 

1008 

1009 

1010def map_ebsd() -> bool: 

1011 """ 

1012 Start an EBSD map and ensure it takes the minimum expected time. 

1013 

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. 

1015 

1016 ## Returns 

1017 

1018 bool 

1019 True if the EBSD mapping is completed successfully. 

1020 

1021 ## Raises 

1022 

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 

1036 

1037 

1038def map_eds() -> bool: 

1039 """ 

1040 Start an EDS map and ensure it takes the minimum expected time. 

1041 

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. 

1043 

1044 ## Returns 

1045 

1046 bool 

1047 True if the EDS mapping is completed successfully. 

1048 

1049 ## Raises 

1050 

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