Coverage for src/pytribeam/laser.py: 32%

247 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2026-09-09 18:27 +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 

164 

165try: 

166 import Laser.PythonControl as tfs_laser 

167 

168 print("Laser PythonControl API imported.") 

169except: 

170 print("WARNING: Laser API not imported!") 

171 print("\tLaser control, as well as EBSD and EDS control are unavailable.") 

172 

173# 3rd party .whl modules 

174 

175# Local scripts 

176from pytribeam.constants import Constants 

177import pytribeam.factory as factory 

178import pytribeam.types as tbt 

179import pytribeam.utilities as ut 

180import pytribeam.insertable_devices as devices 

181import pytribeam.image as img 

182import pytribeam.log as log 

183 

184 

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

186 """ 

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

188 

189 ## Parameters 

190 

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

192 

193 ## Returns 

194 

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

196 """ 

197 db = {} 

198 

199 db["wavelength_nm"] = state.wavelength_nm 

200 db["frequency_khz"] = state.frequency_khz 

201 db["pulse_divider"] = state.pulse_divider 

202 db["pulse_energy_uj"] = state.pulse_energy_uj 

203 db["objective_position_mm"] = state.objective_position_mm 

204 db["expected_pattern_duration_s"] = state.expected_pattern_duration_s 

205 

206 beam_shift = state.beam_shift_um 

207 db["beam_shift_um_x"] = beam_shift.x 

208 db["beam_shift_um_y"] = beam_shift.y 

209 

210 # we can name these differently depending on the needs of the GUI 

211 pattern = state.pattern 

212 db["laser_pattern_mode"] = pattern.mode.value 

213 db["laser_pattern_rotation_deg"] = pattern.rotation_deg 

214 db["laser_pattern_pulses_per_pixel"] = pattern.pulses_per_pixel 

215 db["laser_pattern_pixel_dwell_ms"] = pattern.pixel_dwell_ms 

216 

217 geometry = pattern.geometry 

218 db["passes"] = geometry.passes 

219 db["laser_scan_type"] = geometry.scan_type.value 

220 db["geometry_type"] = geometry.type.value 

221 

222 if geometry.type == tbt.LaserPatternType.BOX: 

223 db["size_x_um"] = geometry.size_x_um 

224 db["size_y_um"] = geometry.size_y_um 

225 db["pitch_x_um"] = geometry.pitch_x_um 

226 db["pitch_y_um"] = geometry.pitch_y_um 

227 db["coordinate_ref"] = geometry.coordinate_ref 

228 

229 if geometry.type == tbt.LaserPatternType.LINE: 229 ↛ 233line 229 didn't jump to line 233 because the condition on line 229 was always true

230 db["size_um"] = geometry.size_um 

231 db["pitch_um"] = geometry.pitch_um 

232 

233 return db 

234 

235 

236def laser_connected() -> bool: 

237 """ 

238 Check if the laser is connected. 

239 

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

241 

242 ## Returns 

243 

244 bool 

245 True if the laser is connected, False otherwise. 

246 

247 """ 

248 connect_msg = "Connection test successful.\n" 

249 laser_status = io.StringIO() 

250 try: 

251 with contextlib.redirect_stdout(laser_status): 

252 tfs_laser.TestConnection() 

253 except: 

254 return False 

255 else: 

256 if laser_status.getvalue() == connect_msg: 

257 return True 

258 return False 

259 

260 

261def _device_connections() -> tbt.DeviceStatus: 

262 """ 

263 Check the connection status of the laser and associated external devices. 

264 

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

266 

267 ## Returns 

268 

269 tbt.DeviceStatus 

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

271 

272 """ 

273 # laser must be connected to connect with other devices: 

274 if not laser_connected(): 

275 laser = tbt.RetractableDeviceState.ERROR 

276 ebsd = tbt.RetractableDeviceState.ERROR 

277 eds = tbt.RetractableDeviceState.ERROR 

278 else: 

279 laser = tbt.RetractableDeviceState.CONNECTED 

280 ebsd = devices.connect_EBSD() # retractable device state 

281 eds = devices.connect_EDS() # retractable device state 

282 

283 return tbt.DeviceStatus( 

284 laser=laser, 

285 ebsd=ebsd, 

286 eds=eds, 

287 ) 

288 

289 

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

291 """ 

292 Set the laser pattern mode. 

293 

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

295 

296 ## Parameters 

297 

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

299 

300 ## Returns 

301 

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

303 

304 ## Raises 

305 

306 - `SystemError`: If the pattern mode cannot be set correctly. 

307 """ 

308 tfs_laser.Patterning_Mode(mode.value) 

309 laser_state = factory.active_laser_state() 

310 if laser_state.pattern.mode != mode: 310 ↛ 312line 310 didn't jump to line 312 because the condition on line 310 was always true

311 raise SystemError("Unable to correctly set pattern mode.") 

312 return True 

313 

314 

315def pulse_energy_uj( 

316 energy_uj: float, 

317 energy_tol_uj: float = Constants.laser_energy_tol_uj, 

318 delay_s: float = 3.0, 

319) -> bool: 

320 """ 

321 Set the pulse energy on the laser. 

322 

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

324 

325 ## Parameters 

326 

327 - `energy_uj` (`float`): The pulse energy to set in microjoules. 

328 - `energy_tol_uj` (`float, optional`): The tolerance for the pulse energy in microjoules (default is Constants.laser_energy_tol_uj). 

329 - `delay_s` (`float, optional`): The delay in seconds after setting the pulse energy (default is 3.0 seconds). 

330 

331 ## Returns 

332 

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

334 

335 ## Raises 

336 

337 - `ValueError`: If the pulse energy cannot be set correctly. 

338 """ 

339 tfs_laser.Laser_SetPulseEnergy_MicroJoules(energy_uj) 

340 time.sleep(delay_s) 

341 laser_state = factory.active_laser_state() 

342 if not ut.in_interval( 

343 val=laser_state.pulse_energy_uj, 

344 limit=tbt.Limit( 

345 min=energy_uj - energy_tol_uj, 

346 max=energy_uj + energy_tol_uj, 

347 ), 

348 type=tbt.IntervalType.CLOSED, 

349 ): 

350 raise ValueError( 

351 f"Could not properly set pulse energy, requested '{energy_uj}' uJ", 

352 f"Current settings is {round(laser_state.pulse_energy_uj, 3)} uJ", 

353 ) 

354 return True 

355 

356 

357def pulse_divider( 

358 divider: int, 

359 delay_s: float = Constants.laser_delay_s, 

360) -> bool: 

361 """ 

362 Set the pulse divider on the laser. 

363 

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

365 

366 ## Parameters 

367 

368 - `divider` (`int`): The pulse divider to set. 

369 - `delay_s` (`float, optional`): The delay in seconds after setting the pulse divider (default is Constants.laser_delay_s). 

370 

371 ## Returns 

372 

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

374 

375 ## Raises 

376 

377 - `ValueError`: If the pulse divider cannot be set correctly. 

378 """ 

379 tfs_laser.Laser_PulseDivider(divider) 

380 time.sleep(delay_s) 

381 laser_state = factory.active_laser_state() 

382 if laser_state.pulse_divider != divider: 

383 raise ValueError( 

384 f"Could not properly set pulse divider, requested '{divider}'", 

385 f"Current settings have a divider of {laser_state.pulse_divider}.", 

386 ) 

387 return True 

388 

389 

390def set_wavelength( 

391 wavelength: tbt.LaserWavelength, 

392 frequency_khz: float = 60, # make constnat 

393 timeout_s: int = 20, # 120, # make constant 

394 num_attempts: int = 2, # TODO make a constant 

395 delay_s: int = 5, # make a constant 

396) -> bool: 

397 """ 

398 Set the wavelength and frequency of the laser. 

399 

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

401 

402 ## Parameters 

403 

404 - `wavelength` (`tbt.LaserWavelength`): The wavelength to set. 

405 - `frequency_khz` (`float, optional`): The frequency to set in kHz (default is 60 kHz). 

406 - `timeout_s` (`int, optional`): The timeout in seconds for each attempt (default is 20 seconds). 

407 - `num_attempts` (`int, optional`): The number of attempts to set the wavelength and frequency (default is 2). 

408 - `delay_s` (`int, optional`): The delay in seconds between checks (default is 5 seconds). 

409 

410 ## Returns 

411 

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

413 """ 

414 

415 def correct_preset(laser_state: tbt.LaserState): 

416 if laser_state.wavelength_nm == wavelength: 

417 return math.isclose(laser_state.frequency_khz, frequency_khz, rel_tol=0.05) 

418 # TODO use constant for tolerance): 

419 return False 

420 

421 for _ in range(num_attempts): 

422 if correct_preset(factory.active_laser_state()): 422 ↛ 424line 422 didn't jump to line 424 because the condition on line 422 was always true

423 return True 

424 print("Adjusting preset...") 

425 tfs_laser.Laser_SetPreset( 

426 wavelength_nm=wavelength.value, frequency_kHz=frequency_khz 

427 ) 

428 time_remaining = timeout_s 

429 while time_remaining > 0: 

430 laser_state = factory.active_laser_state() 

431 # print(time_remaining, laser_state.frequency_khz) 

432 if correct_preset(laser_state=laser_state): 

433 return True 

434 time.sleep(delay_s) 

435 time_remaining -= delay_s 

436 

437 # TODO: This does not verify that the wavelength was set and does not match the other functions here 

438 # Perhaps this should be modified to raise an error if it does not set? 

439 # Might be a lase API thing though? 

440 return False 

441 

442 

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

444 """ 

445 Measure the laser power in watts. 

446 

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

448 

449 ## Parameters 

450 

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

452 

453 ## Returns 

454 

455 - `float`: The measured laser power in watts. 

456 """ 

457 # TODO: Perhaps a try/finally structure would be safer here? 

458 # Unless the laser API ensures that emission is off if it fails? 

459 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON() 

460 # try: 

461 # tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset() 

462 # tfs_laser.Laser_FireContinuously_Start() 

463 # try: 

464 # time.sleep(delay_s) 

465 # return tfs_laser.Laser_ExternalPowerMeter_ReadPower() 

466 # finally: 

467 # tfs_laser.Laser_FireContinuously_Stop() 

468 # finally: 

469 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF() 

470 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON() 

471 tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset() 

472 tfs_laser.Laser_FireContinuously_Start() 

473 time.sleep(delay_s) 

474 power = tfs_laser.Laser_ExternalPowerMeter_ReadPower() 

475 tfs_laser.Laser_FireContinuously_Stop() 

476 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF() 

477 return power 

478 

479 

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

481 """ 

482 Insert the laser shutter. 

483 

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

485 

486 ## Parameters 

487 

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

489 

490 ## Returns 

491 

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

493 

494 ## Raises 

495 

496 - `SystemError`: If the laser shutter cannot be inserted. 

497 """ 

498 devices.CCD_view(microscope=microscope) 

499 if tfs_laser.Shutter_GetState() != "Inserted": 499 ↛ 500,   499 ↛ 5012 missed branches: 1) line 499 didn't jump to line 500 because the condition on line 499 was never true, 2) line 499 didn't jump to line 501 because the condition on line 499 was always true

500 tfs_laser.Shutter_Insert() 

501 state = tfs_laser.Shutter_GetState() 

502 if state != "Inserted": 

503 raise SystemError( 

504 f"Could not insert laser shutter, current laser shutter state is '{state}'." 

505 ) 

506 devices.CCD_pause(microscope=microscope) 

507 return True 

508 

509 

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

511 """ 

512 Retract the laser shutter. 

513 

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

515 

516 ## Parameters 

517 

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

519 

520 ## Returns 

521 

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

523 

524 ## Raises 

525 

526 - `SystemError`: If the laser shutter cannot be retracted. 

527 """ 

528 devices.CCD_view(microscope=microscope) 

529 if tfs_laser.Shutter_GetState() != "Retracted": 

530 tfs_laser.Shutter_Retract() 

531 state = tfs_laser.Shutter_GetState() 

532 if state != "Retracted": 532 ↛ 536line 532 didn't jump to line 536 because the condition on line 532 was always true

533 raise SystemError( 

534 f"Could not retract laser shutter, current laser shutter state is '{state}'." 

535 ) 

536 devices.CCD_pause(microscope=microscope) 

537 return True 

538 

539 

540def pulse_polarization( 

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

542) -> bool: 

543 """ 

544 Configure the polarization of the laser light. 

545 

546 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: 

547 - Waveplate_None switches to Vert. (P) 

548 - Waveplate_1030 switches to Horiz. (S) 

549 - Waveplate_515 switches to Horiz. (S) 

550 

551 ## Parameters 

552 

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

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

555 

556 ## Returns 

557 

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

559 

560 ## Raises 

561 

562 - `KeyError`: If the laser wavelength or pulse polarization is invalid. 

563 """ 

564 if polarization == tbt.LaserPolarization.VERTICAL: 

565 tfs_laser.FlipperConfiguration("Waveplate_None") 

566 return True 

567 elif polarization == tbt.LaserPolarization.HORIZONTAL: 

568 match_db = { 

569 tbt.LaserWavelength.NM_1030: "Waveplate_1030", 

570 tbt.LaserWavelength.NM_515: "Waveplate_515", 

571 } 

572 try: 

573 tfs_laser.FlipperConfiguration(match_db[wavelength]) 

574 except KeyError: 

575 raise KeyError( 

576 f"Invalid laser wavelength, valid options are {[i.value for i in tbt.LaserWavelength]}" 

577 ) 

578 return True 

579 else: 

580 raise KeyError( 

581 f"Invalid pulse polarization, valid options are {[i.value for i in tbt.LaserPolarization]}" 

582 ) 

583 

584 

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

586 """ 

587 Apply the pulse settings to the laser. 

588 

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

590 

591 ## Parameters 

592 

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

594 

595 ## Returns 

596 

597 - `bool`: True if the pulse settings are applied correctly. 

598 """ 

599 active_state = factory.active_laser_state() 

600 if pulse.wavelength_nm != active_state.wavelength_nm: 600 ↛ 602,   600 ↛ 6032 missed branches: 1) line 600 didn't jump to line 602 because the condition on line 600 was never true, 2) line 600 didn't jump to line 603 because the condition on line 600 was always true

601 # wavelength settings 

602 set_wavelength(wavelength=pulse.wavelength_nm) 

603 pulse_divider(divider=pulse.divider) 

604 pulse_energy_uj(energy_uj=pulse.energy_uj) 

605 pulse_polarization(polarization=pulse.polarization, wavelength=pulse.wavelength_nm) 

606 return True 

607 

608 

609def retract_laser_objective() -> bool: 

610 """ 

611 Retract the laser objective to a safe position. 

612 

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

614 

615 ## Returns 

616 

617 bool 

618 True if the laser objective is successfully retracted. 

619 

620 """ 

621 objective_position(position_mm=Constants.laser_objective_retracted_mm) 

622 return True 

623 

624 

625def objective_position( 

626 position_mm: float, 

627 tolerance_mm=Constants.laser_objective_tolerance_mm, 

628) -> bool: 

629 """ 

630 Move the laser objective to the requested position. 

631 

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

633 

634 ## Parameters 

635 

636 - `position_mm` (`float`): The desired position of the laser objective in millimeters. 

637 - `tolerance_mm` (`float, optional`): The tolerance for the laser objective position in millimeters (default is Constants.laser_objective_tolerance_mm). 

638 

639 ## Returns 

640 

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

642 

643 ## Raises 

644 

645 - `ValueError`: If the requested position is out of range. 

646 - `SystemError`: If the laser objective cannot be moved to the requested position. 

647 """ 

648 tfs_laser.LIP_UnlockZ() 

649 

650 if not ut.in_interval( 650 ↛ 659line 650 didn't jump to line 659 because the condition on line 650 was always true

651 val=position_mm, 

652 limit=Constants.laser_objective_limit_mm, 

653 type=tbt.IntervalType.CLOSED, 

654 ): 

655 raise ValueError( 

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

657 ) 

658 

659 for _ in range(2): 

660 if ut.in_interval( 660 ↛ 667,   660 ↛ 6682 missed branches: 1) line 660 didn't jump to line 667 because the condition on line 660 was never true, 2) line 660 didn't jump to line 668 because the condition on line 660 was always true

661 val=tfs_laser.LIP_GetZPosition(), 

662 limit=tbt.Limit( 

663 min=position_mm - tolerance_mm, max=position_mm + tolerance_mm 

664 ), 

665 type=tbt.IntervalType.CLOSED, 

666 ): 

667 return True 

668 tfs_laser.LIP_SetZPosition(position_mm, asynchronously=False) 

669 

670 raise SystemError( 

671 f"Unable to move laser injection port objective to requested position of {position_mm} +/- {tolerance_mm} mm.", 

672 f"Currently at {tfs_laser.LIP_GetZPosition()} mm.", 

673 ) 

674 

675 

676def _shift_axis( 

677 target: float, 

678 current: float, 

679 tolerance: float, 

680 axis: str, 

681) -> bool: 

682 """ 

683 Helper function for beam shift. 

684 

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

686 

687 ## Parameters 

688 

689 - `target` (`float`): The target value for the beam shift. 

690 - `current` (`float`): The current value of the beam shift. 

691 - `tolerance` (`float`): The tolerance for the beam shift. 

692 - `axis` (`str`): The axis to adjust ("X" or "Y"). 

693 

694 ## Returns 

695 

696 - `bool`: True if the beam shift is adjusted to the target value correctly, False otherwise. 

697 """ 

698 for _ in range(2): 698 ↛ 715line 698 didn't jump to line 715 because the loop on line 698 didn't complete

699 if ut.in_interval( 699 ↛ 707,   699 ↛ 7082 missed branches: 1) line 699 didn't jump to line 707 because the condition on line 699 was never true, 2) line 699 didn't jump to line 708 because the condition on line 699 was always true

700 val=current, 

701 limit=tbt.Limit( 

702 min=target - tolerance, 

703 max=target + tolerance, 

704 ), 

705 type=tbt.IntervalType.CLOSED, 

706 ): 

707 return True 

708 if axis == "X": 

709 tfs_laser.BeamShift_Set_X(value=target) 

710 current = tfs_laser.BeamShift_Get_X() 

711 if axis == "Y": 

712 tfs_laser.BeamShift_Set_Y(value=target) 

713 current = tfs_laser.BeamShift_Get_Y() 

714 

715 return False 

716 

717 

718def beam_shift( 

719 shift_um: tbt.Point, 

720 shift_tolerance_um: float = Constants.laser_beam_shift_tolerance_um, 

721) -> bool: 

722 """ 

723 Adjust the laser beam shift to the specified values. 

724 

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

726 

727 ## Parameters 

728 

729 - `shift_um` (`tbt.Point`): The target beam shift values in micrometers. 

730 - `shift_tolerance_um` (`float, optional`): The tolerance for the beam shift in micrometers (default is Constants.laser_beam_shift_tolerance_um). 

731 

732 ## Returns 

733 

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

735 

736 ## Raises 

737 

738 - `ValueError`: If the beam shift cannot be adjusted to the target values. 

739 """ 

740 current_shift_x = tfs_laser.BeamShift_Get_X() 

741 current_shift_y = tfs_laser.BeamShift_Get_Y() 

742 

743 if not ( 743 ↛ 760line 743 didn't jump to line 760 because the condition on line 743 was always true

744 _shift_axis( 

745 target=shift_um.x, 

746 current=current_shift_x, 

747 tolerance=shift_tolerance_um, 

748 axis="X", 

749 ) 

750 and _shift_axis( 

751 target=shift_um.y, 

752 current=current_shift_y, 

753 tolerance=shift_tolerance_um, 

754 axis="Y", 

755 ) 

756 ): 

757 raise ValueError( 

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

759 ) 

760 return True 

761 

762 

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

764 """ 

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

766 

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

768 

769 ## Parameters 

770 

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

772 

773 ## Returns 

774 

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

776 

777 ## Raises 

778 

779 - `ValueError`: If the pattern geometry type is unsupported. 

780 - `SystemError`: If the pattern cannot be set correctly. 

781 """ 

782 pattern_mode(mode=pattern.mode) 

783 

784 # check if pattern is empty or not 

785 if isinstance(pattern.geometry, tbt.LaserBoxPattern): 

786 box = pattern.geometry 

787 tfs_laser.Patterning_CreatePattern_Box( 

788 sizeX_um=box.size_x_um, 

789 sizeY_um=box.size_y_um, 

790 pitchX_um=box.pitch_x_um, 

791 pitchY_um=box.pitch_y_um, 

792 dwellTime_ms=pattern.pixel_dwell_ms, 

793 passes_int=box.passes, 

794 pulsesPerPixel_int=pattern.pulses_per_pixel, 

795 scanrotation_degrees=pattern.rotation_deg, 

796 scantype_string=box.scan_type.value, # cast enum to string 

797 coordinateReference_string=box.coordinate_ref.value, # cast enum to string 

798 ) 

799 elif isinstance(pattern.geometry, tbt.LaserLinePattern): 

800 line = pattern.geometry 

801 tfs_laser.Patterning_CreatePattern_Line( 

802 sizeX_um=line.size_um, 

803 pitchX_um=line.pitch_um, 

804 dwellTime_ms=pattern.pixel_dwell_ms, 

805 passes_int=line.passes, 

806 pulsesPerPixel_int=pattern.pulses_per_pixel, 

807 scanrotation_degrees=pattern.rotation_deg, 

808 scantype_string=line.scan_type.value, # cast enum to string 

809 ) 

810 else: 

811 raise ValueError( 

812 f"Unsupported pattern geometry of type '{type(pattern.geometry)}'. Supported types are {tbt.LaserLinePattern, tbt.LaserBoxPattern}" 

813 ) 

814 laser_state = factory.active_laser_state() 

815 if laser_state.pattern != pattern: 

816 raise SystemError("Unable to correctly set Pattern.") 

817 return True 

818 

819 

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

821 """ 

822 Apply the laser settings to the current patterning. 

823 

824 This function applies the specified laser settings to the current patterning, including beam scan rotation, pulse settings, objective position, beam shift, and patterning settings. 

825 

826 ## Parameters 

827 

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

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

830 

831 ## Returns 

832 

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

834 """ 

835 microscope = settings.microscope 

836 

837 # forces rotation of electron beam for laser (TFS required) 

838 img.beam_scan_rotation( 

839 beam=image_beam, 

840 microscope=microscope, 

841 rotation_deg=Constants.image_scan_rotation_for_laser_deg, 

842 ) 

843 # pulse settings 

844 pulse_settings(pulse=settings.pulse) 

845 

846 # objective position 

847 objective_position(settings.objective_position_mm) 

848 

849 # beam shift 

850 beam_shift(settings.beam_shift_um) 

851 

852 # apply patterning settings 

853 create_pattern(pattern=settings.pattern) 

854 

855 return True 

856 

857 

858def execute_patterning() -> bool: 

859 """ 

860 Execute the laser patterning. 

861 

862 This function starts the laser patterning process. 

863 

864 ## Returns 

865 

866 bool 

867 True if the patterning process is started successfully. 

868 

869 """ 

870 tfs_laser.Patterning_Start() 

871 

872 return True 

873 

874 

875### main methods 

876 

877 

878def mill_region( 

879 settings: tbt.LaserSettings, 

880) -> bool: 

881 """ 

882 Perform laser milling on a specified region. 

883 

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

885 

886 ## Parameters 

887 

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

889 

890 ## Returns 

891 

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

893 

894 ## Raises 

895 

896 - `SystemError`: If the laser is not connected. 

897 """ 

898 # check connection 

899 if not laser_connected(): 

900 raise SystemError("Laser is not connected") 

901 

902 microscope = settings.microscope 

903 # initial_scan_rotation of ebeam 

904 devices.device_access(microscope=microscope) 

905 active_beam = factory.active_beam_with_settings(microscope=microscope) 

906 scan_settings = factory.active_scan_settings(microscope=microscope) 

907 initial_scan_rotation_deg = scan_settings.rotation_deg 

908 

909 # apply laser settings 

910 apply_laser_settings( 

911 image_beam=active_beam, 

912 settings=settings, 

913 ) 

914 

915 # insert shutter 

916 insert_shutter(microscope=microscope) 

917 

918 # execute patterning 

919 execute_patterning() 

920 

921 # retract shutter 

922 retract_shutter(microscope=microscope) 

923 time.sleep(1) 

924 

925 # reset scan rotation 

926 img.beam_scan_rotation( 

927 beam=active_beam, 

928 microscope=microscope, 

929 rotation_deg=initial_scan_rotation_deg, 

930 ) 

931 

932 # TODO: Current code is not very safe here 

933 # Below makes sure that the scan reset happens regardless of what happens during milling 

934 # try: 

935 # apply_laser_settings( 

936 # image_beam=active_beam, 

937 # settings=settings, 

938 # ) 

939 # insert_shutter(microscope=microscope) 

940 # execute_patterning() 

941 # finally: 

942 # try: 

943 # retract_shutter(microscope=microscope) 

944 # finally: 

945 # img.beam_scan_rotation( 

946 # beam=active_beam, 

947 # microscope=microscope, 

948 # rotation_deg=initial_scan_rotation_deg, 

949 # ) 

950 

951 return True 

952 

953 

954def laser_operation( 

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

956) -> bool: 

957 """ 

958 Perform a laser operation based on the specified step and settings. 

959 

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

961 

962 ## Parameters 

963 

964 - `step` (`tbt.Step`): The step object containing the operation settings. 

965 - `general_settings` (`tbt.GeneralSettings`): The general settings object. 

966 - `slice_number` (`int`): The slice number for the operation. 

967 

968 ## Returns 

969 

970 - `bool`: True if the laser operation is completed successfully. 

971 """ 

972 # log laser power before 

973 laser_power_w = read_power() 

974 log.laser_power( 

975 step_number=step.number, 

976 step_name=step.name, 

977 slice_number=slice_number, 

978 log_filepath=general_settings.log_filepath, 

979 dataset_name=Constants.pre_lasing_dataset_name, 

980 power_w=laser_power_w, 

981 ) 

982 

983 mill_region(settings=step.operation_settings) 

984 

985 # log laser power after 

986 laser_power_w = read_power() 

987 log.laser_power( 

988 step_number=step.number, 

989 step_name=step.name, 

990 slice_number=slice_number, 

991 log_filepath=general_settings.log_filepath, 

992 dataset_name=Constants.post_lasing_dataset_name, 

993 power_w=laser_power_w, 

994 ) 

995 

996 return True 

997 

998 

999def map_ebsd() -> bool: 

1000 """ 

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

1002 

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

1004 

1005 ## Returns 

1006 

1007 bool 

1008 True if the EBSD mapping is completed successfully. 

1009 

1010 ## Raises 

1011 

1012 - `ValueError`: If the mapping process does not take the minimum expected time. 

1013 """ 

1014 start_time = time.time() 

1015 tfs_laser.EBSD_StartMap() 

1016 time.sleep(1) 

1017 end_time = time.time() 

1018 map_time = end_time - start_time 

1019 if map_time < Constants.min_map_time_s: 

1020 raise ValueError( 

1021 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EBSD mapping software" 

1022 ) 

1023 print(f"\t\tMapping Complete in {int(map_time)} seconds.") 

1024 return True 

1025 

1026 

1027def map_eds() -> bool: 

1028 """ 

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

1030 

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

1032 

1033 ## Returns 

1034 

1035 bool 

1036 True if the EDS mapping is completed successfully. 

1037 

1038 ## Raises 

1039 

1040 - `ValueError`: If the mapping process does not take the minimum expected time. 

1041 """ 

1042 start_time = time.time() 

1043 tfs_laser.EDS_StartMap() 

1044 time.sleep(1) 

1045 end_time = time.time() 

1046 map_time = end_time - start_time 

1047 if map_time < Constants.min_map_time_s: 

1048 raise ValueError( 

1049 f"Mapping did not take minimum expected time of {Constants.min_map_time_s} seconds, please reset EDS mapping software" 

1050 ) 

1051 print(f"\t\tMapping Complete in {int(map_time)} seconds.") 

1052 return True