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

248 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2026-09-03 19:02 +0000

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 

167except ImportError: 

168 tfs_laser = None 

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

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

171else: 

172 print("Laser PythonControl API imported.") 

173 

174# 3rd party .whl modules 

175 

176# Local scripts 

177from pytribeam.constants import Constants 

178import pytribeam.factory as factory 

179import pytribeam.types as tbt 

180import pytribeam.utilities as ut 

181import pytribeam.insertable_devices as devices 

182import pytribeam.image as img 

183import pytribeam.log as log 

184 

185 

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

187 """ 

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

189 

190 ## Parameters 

191 

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

193 

194 ## Returns 

195 

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

197 """ 

198 db = {} 

199 

200 db["wavelength_nm"] = state.wavelength_nm 

201 db["frequency_khz"] = state.frequency_khz 

202 db["pulse_divider"] = state.pulse_divider 

203 db["pulse_energy_uj"] = state.pulse_energy_uj 

204 db["objective_position_mm"] = state.objective_position_mm 

205 db["expected_pattern_duration_s"] = state.expected_pattern_duration_s 

206 

207 beam_shift = state.beam_shift_um 

208 db["beam_shift_um_x"] = beam_shift.x 

209 db["beam_shift_um_y"] = beam_shift.y 

210 

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

212 pattern = state.pattern 

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

214 db["laser_pattern_rotation_deg"] = pattern.rotation_deg 

215 db["laser_pattern_pulses_per_pixel"] = pattern.pulses_per_pixel 

216 db["laser_pattern_pixel_dwell_ms"] = pattern.pixel_dwell_ms 

217 

218 geometry = pattern.geometry 

219 db["passes"] = geometry.passes 

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

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

222 

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

224 db["size_x_um"] = geometry.size_x_um 

225 db["size_y_um"] = geometry.size_y_um 

226 db["pitch_x_um"] = geometry.pitch_x_um 

227 db["pitch_y_um"] = geometry.pitch_y_um 

228 db["coordinate_ref"] = geometry.coordinate_ref 

229 

230 if geometry.type == tbt.LaserPatternType.LINE: 230 ↛ 231,   230 ↛ 2342 missed branches: 1) line 230 didn't jump to line 231 because the condition on line 230 was never true, 2) line 230 didn't jump to line 234 because the condition on line 230 was always true

231 db["size_um"] = geometry.size_um 

232 db["pitch_um"] = geometry.pitch_um 

233 

234 return db 

235 

236 

237def laser_connected() -> bool: 

238 """ 

239 Check if the laser is connected. 

240 

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

242 

243 ## Returns 

244 

245 bool 

246 True if the laser is connected, False otherwise. 

247 

248 """ 

249 connect_msg = "Connection test successful.\n" 

250 laser_status = io.StringIO() 

251 try: 

252 with contextlib.redirect_stdout(laser_status): 

253 tfs_laser.TestConnection() 

254 except: 

255 return False 

256 else: 

257 if laser_status.getvalue() == connect_msg: 

258 return True 

259 return False 

260 

261 

262def _device_connections() -> tbt.DeviceStatus: 

263 """ 

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

265 

266 This function checks the connection status of the laser, EBSD, and EDS devices. It is meant to be a quick tool for the GUI and does not provide additional information for troubleshooting. 

267 

268 ## Returns 

269 

270 tbt.DeviceStatus 

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

272 

273 """ 

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

275 if not laser_connected(): 275 ↛ 280line 275 didn't jump to line 280 because the condition on line 275 was always true

276 laser = tbt.RetractableDeviceState.ERROR 

277 ebsd = tbt.RetractableDeviceState.ERROR 

278 eds = tbt.RetractableDeviceState.ERROR 

279 else: 

280 laser = tbt.RetractableDeviceState.CONNECTED 

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

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

283 

284 return tbt.DeviceStatus( 

285 laser=laser, 

286 ebsd=ebsd, 

287 eds=eds, 

288 ) 

289 

290 

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

292 """ 

293 Set the laser pattern mode. 

294 

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

296 

297 ## Parameters 

298 

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

300 

301 ## Returns 

302 

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

304 

305 ## Raises 

306 

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

308 """ 

309 tfs_laser.Patterning_Mode(mode.value) 

310 laser_state = factory.active_laser_state() 

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

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

313 return True 

314 

315 

316def pulse_energy_uj( 

317 energy_uj: float, 

318 energy_tol_uj: float = Constants.laser_energy_tol_uj, 

319 delay_s: float = 3.0, 

320) -> bool: 

321 """ 

322 Set the pulse energy on the laser. 

323 

324 This function sets the pulse energy on the laser and verifies that it has been set correctly. It should be done after setting the pulse divider. 

325 

326 ## Parameters 

327 

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

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

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

331 

332 ## Returns 

333 

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

335 

336 ## Raises 

337 

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

339 """ 

340 tfs_laser.Laser_SetPulseEnergy_MicroJoules(energy_uj) 

341 time.sleep(delay_s) 

342 laser_state = factory.active_laser_state() 

343 if not ut.in_interval( 343 ↛ 351,   343 ↛ 3552 missed branches: 1) line 343 didn't jump to line 351 because the condition on line 343 was never true, 2) line 343 didn't jump to line 355 because the condition on line 343 was always true

344 val=laser_state.pulse_energy_uj, 

345 limit=tbt.Limit( 

346 min=energy_uj - energy_tol_uj, 

347 max=energy_uj + energy_tol_uj, 

348 ), 

349 type=tbt.IntervalType.CLOSED, 

350 ): 

351 raise ValueError( 

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

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

354 ) 

355 return True 

356 

357 

358def pulse_divider( 

359 divider: int, 

360 delay_s: float = Constants.laser_delay_s, 

361) -> bool: 

362 """ 

363 Set the pulse divider on the laser. 

364 

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

366 

367 ## Parameters 

368 

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

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

371 

372 ## Returns 

373 

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

375 

376 ## Raises 

377 

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

379 """ 

380 tfs_laser.Laser_PulseDivider(divider) 

381 time.sleep(delay_s) 

382 laser_state = factory.active_laser_state() 

383 if laser_state.pulse_divider != divider: 

384 raise ValueError( 

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

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

387 ) 

388 return True 

389 

390 

391def set_wavelength( 

392 wavelength: tbt.LaserWavelength, 

393 frequency_khz: float = 60, # make constnat 

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

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

396 delay_s: int = 5, # make a constant 

397) -> bool: 

398 """ 

399 Set the wavelength and frequency of the laser. 

400 

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

402 

403 ## Parameters 

404 

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

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

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

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

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

410 

411 ## Returns 

412 

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

414 """ 

415 

416 def correct_preset(laser_state: tbt.LaserState): 

417 if laser_state.wavelength_nm == wavelength: 

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

419 # TODO use constant for tolerance): 

420 return False 

421 

422 for _ in range(num_attempts): 422 ↛ 441line 422 didn't jump to line 441 because the loop on line 422 didn't complete

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

424 return True 

425 print("Adjusting preset...") 

426 tfs_laser.Laser_SetPreset( 

427 wavelength_nm=wavelength.value, frequency_kHz=frequency_khz 

428 ) 

429 time_remaining = timeout_s 

430 while time_remaining > 0: 430 ↛ 422line 430 didn't jump to line 422 because the condition on line 430 was always true

431 laser_state = factory.active_laser_state() 

432 # print(time_remaining, laser_state.frequency_khz) 

433 if correct_preset(laser_state=laser_state): 

434 return True 

435 time.sleep(delay_s) 

436 time_remaining -= delay_s 

437 

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

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

440 # Might be a lase API thing though? 

441 return False 

442 

443 

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

445 """ 

446 Measure the laser power in watts. 

447 

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

449 

450 ## Parameters 

451 

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

453 

454 ## Returns 

455 

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

457 """ 

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

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

460 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON() 

461 # try: 

462 # tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset() 

463 # tfs_laser.Laser_FireContinuously_Start() 

464 # try: 

465 # time.sleep(delay_s) 

466 # return tfs_laser.Laser_ExternalPowerMeter_ReadPower() 

467 # finally: 

468 # tfs_laser.Laser_FireContinuously_Stop() 

469 # finally: 

470 # tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF() 

471 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringON() 

472 tfs_laser.Laser_ExternalPowerMeter_SetZeroOffset() 

473 tfs_laser.Laser_FireContinuously_Start() 

474 time.sleep(delay_s) 

475 power = tfs_laser.Laser_ExternalPowerMeter_ReadPower() 

476 tfs_laser.Laser_FireContinuously_Stop() 

477 tfs_laser.Laser_ExternalPowerMeter_PowerMonitoringOFF() 

478 return power 

479 

480 

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

482 """ 

483 Insert the laser shutter. 

484 

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

486 

487 ## Parameters 

488 

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

490 

491 ## Returns 

492 

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

494 

495 ## Raises 

496 

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

498 """ 

499 devices.CCD_view(microscope=microscope) 

500 if tfs_laser.Shutter_GetState() != "Inserted": 500 ↛ 502line 500 didn't jump to line 502 because the condition on line 500 was always true

501 tfs_laser.Shutter_Insert() 

502 state = tfs_laser.Shutter_GetState() 

503 if state != "Inserted": 503 ↛ 507line 503 didn't jump to line 507 because the condition on line 503 was always true

504 raise SystemError( 

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

506 ) 

507 devices.CCD_pause(microscope=microscope) 

508 return True 

509 

510 

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

512 """ 

513 Retract the laser shutter. 

514 

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

516 

517 ## Parameters 

518 

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

520 

521 ## Returns 

522 

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

524 

525 ## Raises 

526 

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

528 """ 

529 devices.CCD_view(microscope=microscope) 

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

531 tfs_laser.Shutter_Retract() 

532 state = tfs_laser.Shutter_GetState() 

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

534 raise SystemError( 

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

536 ) 

537 devices.CCD_pause(microscope=microscope) 

538 return True 

539 

540 

541def pulse_polarization( 

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

543) -> bool: 

544 """ 

545 Configure the polarization of the laser light. 

546 

547 This function sets the polarization of the laser light based on the specified polarization and wavelength. The polarization is controlled via "FlipperConfiguration", which takes the following values: 

548 - Waveplate_None switches to Vert. (P) 

549 - Waveplate_1030 switches to Horiz. (S) 

550 - Waveplate_515 switches to Horiz. (S) 

551 

552 ## Parameters 

553 

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

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

556 

557 ## Returns 

558 

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

560 

561 ## Raises 

562 

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

564 """ 

565 if polarization == tbt.LaserPolarization.VERTICAL: 

566 tfs_laser.FlipperConfiguration("Waveplate_None") 

567 return True 

568 elif polarization == tbt.LaserPolarization.HORIZONTAL: 

569 match_db = { 

570 tbt.LaserWavelength.NM_1030: "Waveplate_1030", 

571 tbt.LaserWavelength.NM_515: "Waveplate_515", 

572 } 

573 try: 

574 tfs_laser.FlipperConfiguration(match_db[wavelength]) 

575 except KeyError: 

576 raise KeyError( 

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

578 ) 

579 return True 

580 else: 

581 raise KeyError( 

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

583 ) 

584 

585 

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

587 """ 

588 Apply the pulse settings to the laser. 

589 

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

591 

592 ## Parameters 

593 

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

595 

596 ## Returns 

597 

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

599 """ 

600 active_state = factory.active_laser_state() 

601 if pulse.wavelength_nm != active_state.wavelength_nm: 

602 # wavelength settings 

603 set_wavelength(wavelength=pulse.wavelength_nm) 

604 pulse_divider(divider=pulse.divider) 

605 pulse_energy_uj(energy_uj=pulse.energy_uj) 

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

607 return True 

608 

609 

610def retract_laser_objective() -> bool: 

611 """ 

612 Retract the laser objective to a safe position. 

613 

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

615 

616 ## Returns 

617 

618 bool 

619 True if the laser objective is successfully retracted. 

620 

621 """ 

622 objective_position(position_mm=Constants.laser_objective_retracted_mm) 

623 return True 

624 

625 

626def objective_position( 

627 position_mm: float, 

628 tolerance_mm=Constants.laser_objective_tolerance_mm, 

629) -> bool: 

630 """ 

631 Move the laser objective to the requested position. 

632 

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

634 

635 ## Parameters 

636 

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

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

639 

640 ## Returns 

641 

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

643 

644 ## Raises 

645 

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

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

648 """ 

649 tfs_laser.LIP_UnlockZ() 

650 

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

652 val=position_mm, 

653 limit=Constants.laser_objective_limit_mm, 

654 type=tbt.IntervalType.CLOSED, 

655 ): 

656 raise ValueError( 

657 f"Requested laser objective position of {position_mm} mm is out of range. Laser objective can travel from {Constants.laser_objective_limit_mm.min} to {Constants.laser_objective_limit_mm.max} mm." 

658 ) 

659 

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

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

662 val=tfs_laser.LIP_GetZPosition(), 

663 limit=tbt.Limit( 

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

665 ), 

666 type=tbt.IntervalType.CLOSED, 

667 ): 

668 return True 

669 tfs_laser.LIP_SetZPosition(position_mm, asynchronously=False) 

670 

671 raise SystemError( 

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

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

674 ) 

675 

676 

677def _shift_axis( 

678 target: float, 

679 current: float, 

680 tolerance: float, 

681 axis: str, 

682) -> bool: 

683 """ 

684 Helper function for beam shift. 

685 

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

687 

688 ## Parameters 

689 

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

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

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

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

694 

695 ## Returns 

696 

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

698 """ 

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

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

701 val=current, 

702 limit=tbt.Limit( 

703 min=target - tolerance, 

704 max=target + tolerance, 

705 ), 

706 type=tbt.IntervalType.CLOSED, 

707 ): 

708 return True 

709 if axis == "X": 709 ↛ 710,   709 ↛ 7122 missed branches: 1) line 709 didn't jump to line 710 because the condition on line 709 was never true, 2) line 709 didn't jump to line 712 because the condition on line 709 was always true

710 tfs_laser.BeamShift_Set_X(value=target) 

711 current = tfs_laser.BeamShift_Get_X() 

712 if axis == "Y": 

713 tfs_laser.BeamShift_Set_Y(value=target) 

714 current = tfs_laser.BeamShift_Get_Y() 

715 

716 return False 

717 

718 

719def beam_shift( 

720 shift_um: tbt.Point, 

721 shift_tolerance_um: float = Constants.laser_beam_shift_tolerance_um, 

722) -> bool: 

723 """ 

724 Adjust the laser beam shift to the specified values. 

725 

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

727 

728 ## Parameters 

729 

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

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

732 

733 ## Returns 

734 

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

736 

737 ## Raises 

738 

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

740 """ 

741 current_shift_x = tfs_laser.BeamShift_Get_X() 

742 current_shift_y = tfs_laser.BeamShift_Get_Y() 

743 

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

745 _shift_axis( 

746 target=shift_um.x, 

747 current=current_shift_x, 

748 tolerance=shift_tolerance_um, 

749 axis="X", 

750 ) 

751 and _shift_axis( 

752 target=shift_um.y, 

753 current=current_shift_y, 

754 tolerance=shift_tolerance_um, 

755 axis="Y", 

756 ) 

757 ): 

758 raise ValueError( 

759 f"Unable to set laser beam shift. Requested beam shift of (x,y) = ({shift_um.x} um,{shift_um.y} um,), but current beam shift is ({tfs_laser.BeamShift_Get_X()} um, {tfs_laser.BeamShift_Get_Y()} um)." 

760 ) 

761 return True 

762 

763 

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

765 """ 

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

767 

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

769 

770 ## Parameters 

771 

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

773 

774 ## Returns 

775 

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

777 

778 ## Raises 

779 

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

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

782 """ 

783 pattern_mode(mode=pattern.mode) 

784 

785 # check if pattern is empty or not 

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

787 box = pattern.geometry 

788 tfs_laser.Patterning_CreatePattern_Box( 

789 sizeX_um=box.size_x_um, 

790 sizeY_um=box.size_y_um, 

791 pitchX_um=box.pitch_x_um, 

792 pitchY_um=box.pitch_y_um, 

793 dwellTime_ms=pattern.pixel_dwell_ms, 

794 passes_int=box.passes, 

795 pulsesPerPixel_int=pattern.pulses_per_pixel, 

796 scanrotation_degrees=pattern.rotation_deg, 

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

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

799 ) 

800 elif isinstance(pattern.geometry, tbt.LaserLinePattern): 800 ↛ 801,   800 ↛ 8122 missed branches: 1) line 800 didn't jump to line 801 because the condition on line 800 was never true, 2) line 800 didn't jump to line 812 because the condition on line 800 was always true

801 line = pattern.geometry 

802 tfs_laser.Patterning_CreatePattern_Line( 

803 sizeX_um=line.size_um, 

804 pitchX_um=line.pitch_um, 

805 dwellTime_ms=pattern.pixel_dwell_ms, 

806 passes_int=line.passes, 

807 pulsesPerPixel_int=pattern.pulses_per_pixel, 

808 scanrotation_degrees=pattern.rotation_deg, 

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

810 ) 

811 else: 

812 raise ValueError( 

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

814 ) 

815 laser_state = factory.active_laser_state() 

816 if laser_state.pattern != pattern: 

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

818 return True 

819 

820 

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

822 """ 

823 Apply the laser settings to the current patterning. 

824 

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

826 

827 ## Parameters 

828 

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

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

831 

832 ## Returns 

833 

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

835 """ 

836 microscope = settings.microscope 

837 

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

839 img.beam_scan_rotation( 

840 beam=image_beam, 

841 microscope=microscope, 

842 rotation_deg=Constants.image_scan_rotation_for_laser_deg, 

843 ) 

844 # pulse settings 

845 pulse_settings(pulse=settings.pulse) 

846 

847 # objective position 

848 objective_position(settings.objective_position_mm) 

849 

850 # beam shift 

851 beam_shift(settings.beam_shift_um) 

852 

853 # apply patterning settings 

854 create_pattern(pattern=settings.pattern) 

855 

856 return True 

857 

858 

859def execute_patterning() -> bool: 

860 """ 

861 Execute the laser patterning. 

862 

863 This function starts the laser patterning process. 

864 

865 ## Returns 

866 

867 bool 

868 True if the patterning process is started successfully. 

869 

870 """ 

871 tfs_laser.Patterning_Start() 

872 

873 return True 

874 

875 

876### main methods 

877 

878 

879def mill_region( 

880 settings: tbt.LaserSettings, 

881) -> bool: 

882 """ 

883 Perform laser milling on a specified region. 

884 

885 This function performs laser milling on a specified region using the provided laser settings. It checks the laser connection, applies the laser settings, inserts the shutter, executes the patterning, retracts the shutter, and resets the scan rotation. 

886 

887 ## Parameters 

888 

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

890 

891 ## Returns 

892 

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

894 

895 ## Raises 

896 

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

898 """ 

899 # check connection 

900 if not laser_connected(): 

901 raise SystemError("Laser is not connected") 

902 

903 microscope = settings.microscope 

904 # initial_scan_rotation of ebeam 

905 devices.device_access(microscope=microscope) 

906 active_beam = factory.active_beam_with_settings(microscope=microscope) 

907 scan_settings = factory.active_scan_settings(microscope=microscope) 

908 initial_scan_rotation_deg = scan_settings.rotation_deg 

909 

910 # apply laser settings 

911 apply_laser_settings( 

912 image_beam=active_beam, 

913 settings=settings, 

914 ) 

915 

916 # insert shutter 

917 insert_shutter(microscope=microscope) 

918 

919 # execute patterning 

920 execute_patterning() 

921 

922 # retract shutter 

923 retract_shutter(microscope=microscope) 

924 time.sleep(1) 

925 

926 # reset scan rotation 

927 img.beam_scan_rotation( 

928 beam=active_beam, 

929 microscope=microscope, 

930 rotation_deg=initial_scan_rotation_deg, 

931 ) 

932 

933 # TODO: Current code is not very safe here 

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

935 # try: 

936 # apply_laser_settings( 

937 # image_beam=active_beam, 

938 # settings=settings, 

939 # ) 

940 # insert_shutter(microscope=microscope) 

941 # execute_patterning() 

942 # finally: 

943 # try: 

944 # retract_shutter(microscope=microscope) 

945 # finally: 

946 # img.beam_scan_rotation( 

947 # beam=active_beam, 

948 # microscope=microscope, 

949 # rotation_deg=initial_scan_rotation_deg, 

950 # ) 

951 

952 return True 

953 

954 

955def laser_operation( 

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

957) -> bool: 

958 """ 

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

960 

961 This function performs a laser operation using the provided step and general settings. It logs the laser power before and after the operation, and performs the milling process. 

962 

963 ## Parameters 

964 

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

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

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

968 

969 ## Returns 

970 

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

972 """ 

973 # log laser power before 

974 laser_power_w = read_power() 

975 log.laser_power( 

976 step_number=step.number, 

977 step_name=step.name, 

978 slice_number=slice_number, 

979 log_filepath=general_settings.log_filepath, 

980 dataset_name=Constants.pre_lasing_dataset_name, 

981 power_w=laser_power_w, 

982 ) 

983 

984 mill_region(settings=step.operation_settings) 

985 

986 # log laser power after 

987 laser_power_w = read_power() 

988 log.laser_power( 

989 step_number=step.number, 

990 step_name=step.name, 

991 slice_number=slice_number, 

992 log_filepath=general_settings.log_filepath, 

993 dataset_name=Constants.post_lasing_dataset_name, 

994 power_w=laser_power_w, 

995 ) 

996 

997 return True 

998 

999 

1000def map_ebsd() -> bool: 

1001 """ 

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

1003 

1004 This function starts an EBSD map and checks that the mapping process takes at least the minimum expected time. If the mapping process is too short, it raises an error. 

1005 

1006 ## Returns 

1007 

1008 bool 

1009 True if the EBSD mapping is completed successfully. 

1010 

1011 ## Raises 

1012 

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

1014 """ 

1015 start_time = time.time() 

1016 tfs_laser.EBSD_StartMap() 

1017 time.sleep(1) 

1018 end_time = time.time() 

1019 map_time = end_time - start_time 

1020 if map_time < Constants.min_map_time_s: 

1021 raise ValueError( 

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

1023 ) 

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

1025 return True 

1026 

1027 

1028def map_eds() -> bool: 

1029 """ 

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

1031 

1032 This function starts an EDS map and checks that the mapping process takes at least the minimum expected time. If the mapping process is too short, it raises an error. 

1033 

1034 ## Returns 

1035 

1036 bool 

1037 True if the EDS mapping is completed successfully. 

1038 

1039 ## Raises 

1040 

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

1042 """ 

1043 start_time = time.time() 

1044 tfs_laser.EDS_StartMap() 

1045 time.sleep(1) 

1046 end_time = time.time() 

1047 map_time = end_time - start_time 

1048 if map_time < Constants.min_map_time_s: 

1049 raise ValueError( 

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

1051 ) 

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

1053 return True