Coverage for src/pytribeam/factory.py: 43%

682 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2026-10-23 20:42 +0000

1#!/usr/bin/python3 

2"""Factory and validation functions for `pytribeam` settings objects. 

3 

4This module converts raw configuration dictionaries and live microscope state 

5into structured `pytribeam.types` objects. It is the main construction layer used 

6by the workflow to validate YAML input, read current hardware settings, enforce 

7limits, and create typed settings for imaging, FIB, laser, EBSD, EDS, custom, 

8stage, beam, detector, and scan operations. 

9 

10Most users should not need to call the low-level validation helpers directly. 

11Workflow code typically enters this module through `general`, `step`, or one of 

12the `active_*` functions. 

13 

14## Typical usage 

15 

16Create experiment-wide general settings from a parsed YAML dictionary: 

17 

18```python 

19import pytribeam.factory as factory 

20 

21general_settings = factory.general( 

22 general_db=general_db, 

23 yml_format=yml_format, 

24) 

25``` 

26 

27Create a typed workflow step from a YAML step dictionary: 

28 

29```python 

30step = factory.step( 

31 microscope=microscope, 

32 step_name=step_name, 

33 step_settings=step_settings, 

34 general_settings=general_settings, 

35 yml_format=yml_format, 

36) 

37``` 

38 

39Read the current microscope state into `pytribeam` settings objects: 

40 

41```python 

42beam = factory.active_beam_with_settings(microscope) 

43image_settings = factory.active_image_settings(microscope) 

44stage_position = factory.active_stage_position_settings(microscope) 

45``` 

46 

47## Main entry points 

48 

49- `general`: validate and create `tbt.GeneralSettings`. 

50- `step`: validate and create a `tbt.Step` for the configured step type. 

51- `active_image_settings`: read the current microscope imaging state. 

52- `active_beam_with_settings`: read the active beam and beam settings. 

53- `active_detector_settings`: read the active detector settings. 

54- `active_scan_settings`: read the active scan settings. 

55- `active_stage_position_settings`: read the current stage position in user 

56 units. 

57- `active_laser_state`: read the current laser state. 

58- `stage_limits`, `beam_limits`, and `scan_limits`: read hardware limits from 

59 the active microscope connection. 

60 

61## Configuration workflow 

62 

63The workflow module uses this factory layer to transform parsed YAML data into 

64validated runtime objects: 

65 

66```text 

67YAML file 

68 -> raw dictionaries 

69 -> validation helpers 

70 -> typed settings objects 

71 -> workflow execution 

72``` 

73 

74For example, image, FIB, laser, EBSD, EDS, and custom steps are all constructed 

75from dictionaries using step-type-specific factory functions. These functions 

76validate required keys, convert strings to enums, check numeric limits, and 

77return the appropriate `tbt.*Settings` object. 

78 

79## Validation behavior 

80 

81Validation helpers check user configuration against package schema information, 

82supported enum values, microscope hardware limits, and workflow constraints. 

83Invalid settings generally raise `ValueError`, `TypeError`, `KeyError`, or 

84`NotImplementedError` with context about the step or setting that failed. 

85 

86## Active-state helpers 

87 

88Functions beginning with `active_` read the current microscope or laser state and 

89return a corresponding `pytribeam.types` object. These are useful for logging, 

90GUI display, default-setting generation, and validating that requested hardware 

91changes were applied successfully. 

92 

93## Units 

94 

95Factory functions preserve the package-wide unit conventions used by 

96`pytribeam.types`. User-facing values generally use explicit suffixes such as 

97`_mm`, `_um`, `_deg`, `_kv`, `_na`, and `_us`, while values read from or written 

98to hardware APIs may be converted internally as needed. 

99 

100> **Warning** 

101> 

102> Many functions in this module query live microscope or laser state. Ensure that 

103> the required hardware connections and external APIs are available before using 

104> active-state or hardware-limit factory functions. 

105 

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

107""" 

108 

109__all__ = [ 

110 "active_fib_applications", 

111 "active_beam_with_settings", 

112 "active_detector_settings", 

113 "active_image_settings", 

114 "active_imaging_device", 

115 "active_scan_settings", 

116 "active_stage_position_settings", 

117 "active_laser_state", 

118 "active_laser_settings", 

119 "available_detector_types", 

120 "available_detector_modes", 

121 "beam_object_type", 

122 "stage_limits", 

123 "beam_limits", 

124 "scan_limits", 

125 "general", 

126 "image", 

127 "fib", 

128 "laser", 

129 "ebsd", 

130 "eds", 

131 "custom", 

132 "step", 

133 "string_to_res", 

134 "valid_string_resolution", 

135] 

136 

137## python standard libraries 

138from pathlib import Path 

139from typing import List, Union 

140import warnings 

141from functools import singledispatch 

142import math 

143 

144 

145# 3rd party libraries 

146from schema import And, Or, Schema 

147 

148# Local 

149import pytribeam.insertable_devices as devices 

150import pytribeam.image as img 

151import pytribeam.utilities as ut 

152import pytribeam.stage as stage 

153from pytribeam.constants import Conversions, Constants 

154from pytribeam.utilities import application_files 

155 

156try: 

157 import pytribeam.laser as fs_laser 

158except: 

159 pass 

160import pytribeam.types as tbt 

161 

162 

163def active_fib_applications( 

164 microscope: tbt.Microscope, 

165) -> list: 

166 """ 

167 Retrieve a list of all active FIB (Focused Ion Beam) patterning application files from the microscope. 

168 

169 ## Parameters 

170 

171 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the application files. 

172 

173 ## Returns 

174 

175 - `list`: A list of active FIB patterning application files. 

176 

177 """ 

178 return application_files(microscope) 

179 

180 

181def active_beam_with_settings( 

182 microscope: tbt.Microscope, 

183) -> tbt.Beam: 

184 """ 

185 Retrieve the current active beam and its settings from the microscope to create a beam object. 

186 

187 This function grabs the current beam and its settings on the microscope to make a beam object. These settings fully depend on the currently active beam as determined by xTUI. Tolerance values for voltage and current are auto-populated as a ratio of current values predetermined in the `Constants` class. 

188 

189 ## Parameters 

190 

191 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active beam and its settings. 

192 

193 ## Returns 

194 

195 - `tbt.Beam`: The active beam object with its settings. 

196 """ 

197 selected_beam = active_imaging_device(microscope=microscope) 

198 beam = ut.beam_type(selected_beam, microscope) 

199 

200 voltage_kv = round(beam.high_voltage.value * Conversions.V_TO_KV, 6) 

201 voltage_tol_kv = round(voltage_kv * Constants.voltage_tol_ratio, 6) 

202 

203 current_na = round(beam.beam_current.value * Conversions.A_TO_NA, 6) 

204 current_tol_na = round(current_na * Constants.current_tol_ratio, 6) 

205 

206 hfw_mm = round(beam.horizontal_field_width.value * Conversions.M_TO_MM, 6) 

207 

208 working_dist_mm = round(beam.working_distance.value * Conversions.M_TO_MM, 6) 

209 

210 angular_correction = getattr(beam, "angular_correction", None) 

211 if angular_correction is None: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true

212 dynamic_focus = None 

213 tilt_correction = None 

214 else: 

215 dynamic_focus = bool(angular_correction.dynamic_focus.is_on) 

216 tilt_correction = bool(angular_correction.tilt_correction.is_on) 

217 

218 active_settings = tbt.BeamSettings( 

219 voltage_kv=voltage_kv, 

220 current_na=current_na, 

221 hfw_mm=hfw_mm, 

222 working_dist_mm=working_dist_mm, 

223 voltage_tol_kv=voltage_tol_kv, 

224 current_tol_na=current_tol_na, 

225 dynamic_focus=dynamic_focus, 

226 tilt_correction=tilt_correction, 

227 ) 

228 

229 return type(selected_beam)(settings=active_settings) 

230 

231 

232def active_detector_settings( 

233 microscope: tbt.Microscope, 

234) -> tbt.Detector: 

235 """ 

236 Retrieve the current active detector settings from the microscope to create a detector object. 

237 

238 This function grabs the current detector settings on the microscope to make a detector object. These settings fully depend on the currently active detector as determined by xTUI. 

239 

240 If the microscope reports a detector mode that the detector does not offer (for example "None" for a single-mode detector), the detector's first available mode is used instead and a warning is issued, so that the settings pass `validate_detector_settings`. 

241 

242 ## Parameters 

243 

244 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active detector settings. 

245 

246 ## Returns 

247 

248 - `tbt.Detector`: The active detector object with its settings. 

249 """ 

250 

251 detector_type = microscope.detector.type.value 

252 detector_mode = microscope.detector.mode.value 

253 # The microscope can report a mode the detector does not offer (e.g. "None" 

254 # for a single-mode detector). Report one it does, so these settings pass 

255 # validate_detector_settings. 

256 available_modes = list(microscope.detector.mode.available_values) 

257 if available_modes and detector_mode not in available_modes: 257 ↛ 258,   257 ↛ 2642 missed branches: 1) line 257 didn't jump to line 258 because the condition on line 257 was never true, 2) line 257 didn't jump to line 264 because the condition on line 257 was always true

258 warnings.warn( 

259 f'Detector mode "{detector_mode}" is not available on the ' 

260 f'"{detector_type}" detector; using "{available_modes[0]}". ' 

261 f"Available modes: {available_modes}." 

262 ) 

263 detector_mode = available_modes[0] 

264 brightness = microscope.detector.brightness.value 

265 contrast = microscope.detector.contrast.value 

266 auto_cb_settings = tbt.ScanArea( 

267 left=None, 

268 top=None, 

269 width=None, 

270 height=None, 

271 ) 

272 custom_settings = None 

273 

274 active_detector = tbt.Detector( 

275 type=tbt.DetectorType(detector_type), 

276 mode=tbt.DetectorMode(detector_mode), 

277 brightness=brightness, 

278 contrast=contrast, 

279 auto_cb_settings=auto_cb_settings, 

280 custom_settings=custom_settings, 

281 ) 

282 

283 return active_detector 

284 

285 

286def active_image_settings(microscope: tbt.Microscope) -> tbt.ImageSettings: 

287 """ 

288 Retrieve the current active image settings from the microscope to create an image settings object. 

289 

290 This function grabs the current beam, detector, and scan settings on the microscope to make an image settings object. The bit depth is set to the default color depth defined in the `Constants` class. 

291 

292 ## Parameters 

293 

294 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active image settings. 

295 

296 ## Returns 

297 

298 - `tbt.ImageSettings`: The active image settings object. 

299 """ 

300 beam = active_beam_with_settings(microscope=microscope) 

301 detector = active_detector_settings(microscope=microscope) 

302 scan = active_scan_settings(microscope=microscope) 

303 bit_depth = Constants.default_color_depth 

304 

305 active_image_settings = tbt.ImageSettings( 

306 microscope=microscope, 

307 beam=beam, 

308 detector=detector, 

309 scan=scan, 

310 bit_depth=bit_depth, 

311 ) 

312 

313 return active_image_settings 

314 

315 

316def active_imaging_device(microscope: tbt.Microscope) -> tbt.Beam: 

317 """ 

318 Determine the active imaging device and return the corresponding internal beam type object with null beam settings. 

319 

320 This function identifies the currently active imaging device on the microscope and returns the appropriate beam type object (electron or ion) with null beam settings. 

321 

322 ## Parameters 

323 

324 - `microscope` (`tbt.Microscope`): The microscope object from which to determine the active imaging device. 

325 

326 ## Returns 

327 

328 - `tbt.Beam`: The active beam object with null beam settings. 

329 

330 ## Raises 

331 

332 - `ValueError`: If the currently selected device is neither an electron beam nor an ion beam. 

333 """ 

334 curr_device = tbt.Device(microscope.imaging.get_active_device()) 

335 if curr_device == tbt.Device.ELECTRON_BEAM: 

336 selected_beam = beam_object_type(type=tbt.BeamType.ELECTRON)( 

337 settings=tbt.BeamSettings() 

338 ) 

339 elif curr_device == tbt.Device.ION_BEAM: 339 ↛ 340,   339 ↛ 3442 missed branches: 1) line 339 didn't jump to line 340 because the condition on line 339 was never true, 2) line 339 didn't jump to line 344 because the condition on line 339 was always true

340 selected_beam = beam_object_type(type=tbt.BeamType.ION)( 

341 settings=tbt.BeamSettings() 

342 ) 

343 else: 

344 raise ValueError( 

345 f"Currently selected device {curr_device}, make sure a quadrant in xTUI with either an Electron beam or Ion beam active is selected." 

346 ) 

347 return selected_beam 

348 

349 

350def active_scan_settings( 

351 microscope: tbt.Microscope, 

352) -> tbt.Scan: 

353 """ 

354 Retrieve the current active scan settings from the microscope to create a scan object. 

355 

356 This function grabs the current scan settings on the microscope to make a scan object. These settings fully depend on the currently active scan settings as determined by xTUI. 

357 

358 ## Parameters 

359 

360 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active scan settings. 

361 

362 ## Returns 

363 

364 - `tbt.Scan`: The active scan object with its settings. 

365 """ 

366 selected_beam = active_imaging_device(microscope=microscope) 

367 beam = ut.beam_type(selected_beam, microscope) 

368 

369 rotation_deg = beam.scanning.rotation.value * Conversions.RAD_TO_DEG 

370 dwell_time_us = beam.scanning.dwell_time.value * Conversions.S_TO_US 

371 scan_res = beam.scanning.resolution.value 

372 scan_res = string_to_res(scan_res) 

373 if ut.valid_enum_entry(obj=scan_res, check_type=tbt.PresetResolution): 

374 scan_res = tbt.PresetResolution(scan_res) 

375 

376 active_scan = tbt.Scan( 

377 rotation_deg=rotation_deg, 

378 dwell_time_us=dwell_time_us, 

379 resolution=scan_res, 

380 ) 

381 

382 return active_scan 

383 

384 

385def active_stage_position_settings(microscope: tbt.Microscope) -> tbt.StagePositionUser: 

386 """ 

387 Retrieve the current stage position in the raw coordinate system and user units [mm, deg]. 

388 

389 This function sets the stage coordinate system to RAW, retrieves the current stage position in encoder units (meters and radians), converts it to user units (millimeters and degrees), and ensures the r-axis is within the axis limit. 

390 

391 ## Parameters 

392 

393 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the current stage position. 

394 

395 ## Returns 

396 

397 - `tbt.StagePositionUser`: The current stage position in user units [mm, deg]. 

398 """ 

399 stage.coordinate_system(microscope=microscope, mode=tbt.StageCoordinateSystem.RAW) 

400 # encoder positions (pos) are in meters and radians 

401 direct_encoder_pos = microscope.specimen.stage.current_position 

402 x_m, y_m, z_m = direct_encoder_pos.x, direct_encoder_pos.y, direct_encoder_pos.z 

403 r_rad, t_rad = direct_encoder_pos.r, direct_encoder_pos.t 

404 coord_system_str = direct_encoder_pos.coordinate_system 

405 

406 encoder_pos = tbt.StagePositionEncoder( 

407 x=x_m, y=y_m, z=z_m, r=r_rad, t=t_rad, coordinate_system=coord_system_str 

408 ) 

409 user_pos = stage.encoder_to_user_position(encoder_pos) 

410 

411 # ensure r-axis is kept in axis limit 

412 # ------------------------------------------------ 

413 # THIS IS NOT NEEDED, AUTOSCRIPT DOES THIS ALREADY 

414 # ------------------------------------------------ 

415 # if not ut.in_interval( 

416 # val=user_pos.r_deg, 

417 # limit=Constants.rotation_axis_limit_deg, 

418 # type=tbt.IntervalType.RIGHT_OPEN, 

419 # ): 

420 # # used as right-open internal: 180.0 is not valid and should be converted to -180.0 

421 # while user_pos.r_deg >= Constants.rotation_axis_limit_deg.max: 

422 # new_r_deg = user_pos.r_deg - 360.0 

423 # while user_pos.r_deg < Constants.rotation_axis_limit_deg.min: 

424 # new_r_deg = user_pos.r_deg + 360.0 

425 # new_r_deg = round(new_r_deg, 6) 

426 # user_pos = tbt.StagePositionUser( 

427 # x_mm=user_pos.x_mm, 

428 # y_mm=user_pos.y_mm, 

429 # z_mm=user_pos.z_mm, 

430 # r_deg=new_r_deg, 

431 # t_deg=user_pos.t_deg, 

432 # ) 

433 

434 return user_pos 

435 

436 

437def active_laser_state() -> tbt.LaserState: 

438 """ 

439 Retrieve the current state of the laser, including various properties that can be quickly read. 

440 

441 This function returns a dictionary object for all properties that can be quickly read from the laser (not exhaustive). Power can be read but has its own method and is more involved. Flipper configuration can only be set, not read. 

442 

443 ## Returns 

444 

445 tbt.LaserState 

446 The current state of the laser, including wavelength, frequency, pulse divider, pulse energy, objective position, beam shift, pattern, and expected pattern duration. 

447 

448 ## Raises 

449 

450 KeyError 

451 If an unsupported LaserPatternType is encountered. 

452 

453 """ 

454 vals = fs_laser.tfs_laser.Laser_ReadValues() 

455 vals["objective_position_mm"] = fs_laser.tfs_laser.LIP_GetZPosition() 

456 vals["beam_shift_um_x"] = fs_laser.tfs_laser.BeamShift_Get_X() 

457 vals["beam_shift_um_y"] = fs_laser.tfs_laser.BeamShift_Get_Y() 

458 vals["shutter_state"] = fs_laser.tfs_laser.Shutter_GetState() 

459 vals["pattern"] = fs_laser.tfs_laser.Patterning_ReadValues() 

460 vals["expected_pattern_duration_s"] = ( 

461 fs_laser.tfs_laser.Patterning_GetExpectedDuration() 

462 ) 

463 

464 pattern_db = vals["pattern"] 

465 pattern_type = pattern_db["patternType"].lower() 

466 if not ut.valid_enum_entry(pattern_type, tbt.LaserPatternType): 

467 raise KeyError( 467 ↛ exit,   467 ↛ exit2 missed branches: 1) line 467 didn't jump to the function exit, 2) line 467 didn't except from function 'active_laser_state' because the raise on line 467 wasn't executed

468 f"Unsupported LaserPatternType of {pattern_type}, supported types include: {[i.value for i in tbt.LaserPatternType]}" 

469 ) 

470 pattern_type = tbt.LaserPatternType(pattern_type) 

471 mode = tbt.LaserPatternMode(pattern_db["patterningMode"].lower()) 

472 if mode == tbt.LaserPatternMode.COARSE: 472 ↛ 475line 472 didn't jump to line 475 because the condition on line 472 was always true

473 pixel_dwell_ms = pattern_db["dwellTime"] 

474 pulses_per_pixel = None 

475 elif mode == tbt.LaserPatternMode.FINE: 475 ↛ 478line 475 didn't jump to line 478 because the condition on line 475 was always true

476 pixel_dwell_ms = None 

477 pulses_per_pixel = pattern_db["pulsesPerPixel"] 

478 rotation_deg = pattern_db["patternRotation_deg"] 

479 

480 if pattern_type == tbt.LaserPatternType.BOX: 480 ↛ 492line 480 didn't jump to line 492 because the condition on line 480 was always true

481 geometry = tbt.LaserBoxPattern( 

482 passes=pattern_db["passes"], 

483 size_x_um=pattern_db["xSize_um"], 

484 size_y_um=pattern_db["ySize_um"], 

485 pitch_x_um=pattern_db["xPitch_um"], 

486 pitch_y_um=pattern_db["yPitch_um"], 

487 scan_type=tbt.LaserScanType(pattern_db["scanningMode"].lower()), 

488 coordinate_ref=tbt.CoordinateReference( 

489 pattern_db["coordReference"].lower() 

490 ), 

491 ) 

492 if pattern_type == tbt.LaserPatternType.LINE: 492 ↛ 493,   492 ↛ 5002 missed branches: 1) line 492 didn't jump to line 493 because the condition on line 492 was never true, 2) line 492 didn't jump to line 500 because the condition on line 492 was always true

493 geometry = tbt.LaserLinePattern( 

494 passes=pattern_db["passes"], 

495 size_um=pattern_db["xSize_um"], 

496 pitch_um=pattern_db["xPitch_um"], 

497 scan_type=tbt.LaserScanType(pattern_db["scanningMode"].lower()), 

498 ) 

499 

500 pattern = tbt.LaserPattern( 

501 mode=mode, 

502 rotation_deg=rotation_deg, 

503 geometry=geometry, 

504 pulses_per_pixel=pulses_per_pixel, 

505 pixel_dwell_ms=pixel_dwell_ms, 

506 ) 

507 

508 state = tbt.LaserState( 

509 wavelength_nm=vals["wavelength_nm"], 

510 frequency_khz=vals["frequency_kHz"], 

511 pulse_divider=vals["pulse_divider"], 

512 pulse_energy_uj=vals["pulse_energy_uJ"], 

513 objective_position_mm=vals["objective_position_mm"], 

514 beam_shift_um=tbt.Point(x=vals["beam_shift_um_x"], y=vals["beam_shift_um_y"]), 

515 pattern=pattern, 

516 expected_pattern_duration_s=vals["expected_pattern_duration_s"], 

517 ) 

518 

519 return state 

520 

521 

522def active_laser_settings(microscope: tbt.Microscope) -> tbt.LaserSettings: 

523 """ 

524 Retrieve the current active laser settings from the microscope to create a laser settings object. 

525 

526 This function grabs the current laser state and uses it to create a laser settings object. Some values cannot be read by Laser Control and can only be set. For example, polarization will default to "Vertical" as this value cannot be read. 

527 

528 ## Parameters 

529 

530 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the active laser settings. 

531 

532 ## Returns 

533 

534 - `tbt.LaserSettings`: The active laser settings object. 

535 """ 

536 state = active_laser_state() 

537 

538 settings = tbt.LaserSettings( 

539 microscope=microscope, 

540 pulse=tbt.LaserPulse( 

541 wavelength_nm=state.wavelength_nm, 

542 divider=state.pulse_divider, 

543 energy_uj=state.pulse_energy_uj, 

544 polarization=tbt.LaserPolarization.VERTICAL, # TODO, can't read this, can only set it 

545 ), 

546 objective_position_mm=state.objective_position_mm, 

547 beam_shift_um=state.beam_shift_um, 

548 pattern=state.pattern, 

549 ) 

550 

551 return settings 

552 

553 

554def available_detector_types(microscope: tbt.Microscope) -> List[str]: 

555 """ 

556 Retrieve the available detector types on the current microscope. 

557 

558 This function returns a list of available detector types on the current microscope. 

559 

560 ## Parameters 

561 

562 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the available detector types. 

563 

564 ## Returns 

565 

566 - `List[str]`: A list of available detector types. 

567 """ 

568 detectors = microscope.detector.type.available_values 

569 return detectors 

570 

571 

572def available_detector_modes(microscope: tbt.Microscope) -> List[str]: 

573 """ 

574 Retrieve the available detector modes on the current microscope. 

575 

576 This function returns a list of available detector modes on the current microscope. 

577 

578 ## Parameters 

579 

580 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the available detector modes. 

581 

582 ## Returns 

583 

584 - `List[str]`: A list of available detector modes. 

585 """ 

586 modes = microscope.detector.mode.available_values 

587 # available = [tbt.DetectorType(i) for i in modes] 

588 return modes 

589 

590 

591def beam_object_type(type: tbt.BeamType) -> tbt.Beam: 

592 """ 

593 Retrieve the beam object type based on the given beam type. 

594 

595 This function returns the appropriate beam object type (electron or ion) based on the provided beam type. 

596 

597 ## Parameters 

598 

599 - `type` (`tbt.BeamType`): The type of the beam (electron or ion). 

600 

601 ## Returns 

602 

603 - `tbt.Beam`: The corresponding beam object type. 

604 

605 ## Raises 

606 

607 - `NotImplementedError`: If the provided beam type is unsupported. 

608 """ 

609 if not isinstance(type, tbt.BeamType): 

610 raise ValueError("Electron and Ion are the only allowed beam object types.") 

611 if type.value == "electron": 

612 return tbt.ElectronBeam 

613 if type.value == "ion": 

614 return tbt.IonBeam 

615 

616 

617def stage_limits(microscope: tbt.Microscope) -> tbt.StageLimits: 

618 """ 

619 Retrieve the stage limits from the current microscope connection. 

620 

621 This function retrieves the stage limits for the X, Y, Z, R, and T axes from the current microscope connection and returns them as a `StageLimits` object. 

622 

623 ## Parameters 

624 

625 - `microscope` (`tbt.Microscope`): The microscope object from which to retrieve the stage limits. 

626 

627 ## Returns 

628 

629 - `tbt.StageLimits`: The stage limits for the X, Y, Z, R, and T axes in user units (mm and degrees). 

630 """ 

631 stage.coordinate_system(microscope=microscope) 

632 # x position 

633 min_x_mm = ( 

634 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.X).min 

635 * Conversions.M_TO_MM 

636 ) 

637 max_x_mm = ( 

638 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.X).max 

639 * Conversions.M_TO_MM 

640 ) 

641 # y position 

642 min_y_mm = ( 

643 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.Y).min 

644 * Conversions.M_TO_MM 

645 ) 

646 max_y_mm = ( 

647 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.Y).max 

648 * Conversions.M_TO_MM 

649 ) 

650 # z position 

651 min_z_mm = ( 

652 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.Z).min 

653 * Conversions.M_TO_MM 

654 ) 

655 max_z_mm = ( 

656 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.Z).max 

657 * Conversions.M_TO_MM 

658 ) 

659 # r position 

660 min_r_deg = Constants.rotation_axis_limit_deg.min 

661 max_r_deg = Constants.rotation_axis_limit_deg.max 

662 # t position 

663 min_t_deg = ( 

664 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.T).min 

665 * Conversions.RAD_TO_DEG 

666 ) 

667 max_t_deg = ( 

668 microscope.specimen.stage.get_axis_limits(tbt.StageAxis.T).max 

669 * Conversions.RAD_TO_DEG 

670 ) 

671 

672 return tbt.StageLimits( 

673 x_mm=tbt.Limit(min=min_x_mm, max=max_x_mm), 

674 y_mm=tbt.Limit(min=min_y_mm, max=max_y_mm), 

675 z_mm=tbt.Limit(min=min_z_mm, max=max_z_mm), 

676 r_deg=tbt.Limit(min=min_r_deg, max=max_r_deg), 

677 t_deg=tbt.Limit(min=min_t_deg, max=max_t_deg), 

678 ) 

679 

680 

681def beam_limits( 

682 selected_beam: property, 

683 beam_type: tbt.BeamType, 

684) -> tbt.BeamLimits: 

685 """ 

686 Retrieve the beam limits for the selected beam and beam type. 

687 

688 This function retrieves the limits for voltage, current, horizontal field width (HFW), and working distance for the selected beam and beam type, and returns them as a `BeamLimits` object. 

689 

690 ## Parameters 

691 

692 - `selected_beam` (`property`): The selected beam property from which to retrieve the limits. 

693 - `beam_type` (`tbt.BeamType`): The type of the beam (electron or ion). 

694 

695 ## Returns 

696 

697 - `tbt.BeamLimits`: The beam limits for voltage, current, HFW, and working distance in user units (kV, nA, mm). 

698 

699 ## Raises 

700 

701 - `ValueError`: If the beam type is unsupported. 

702 """ 

703 # voltage range 

704 min_kv = selected_beam.high_voltage.limits.min * Conversions.V_TO_KV 

705 max_kv = selected_beam.high_voltage.limits.max * Conversions.V_TO_KV 

706 

707 # current range 

708 if beam_type == tbt.BeamType.ELECTRON: 708 ↛ 711line 708 didn't jump to line 711 because the condition on line 708 was always true

709 min_na = selected_beam.beam_current.limits.min * Conversions.A_TO_NA 

710 max_na = selected_beam.beam_current.limits.max * Conversions.A_TO_NA 

711 if beam_type == tbt.BeamType.ION: 711 ↛ 717line 711 didn't jump to line 717 because the condition on line 711 was always true

712 available_currents = selected_beam.beam_current.available_values 

713 min_na = min(available_currents) * Conversions.A_TO_NA 

714 max_na = max(available_currents) * Conversions.A_TO_NA 

715 

716 # hfw range 

717 min_hfw_mm = selected_beam.horizontal_field_width.limits.min * Conversions.M_TO_MM 

718 max_hfw_mm = selected_beam.horizontal_field_width.limits.max * Conversions.M_TO_MM 

719 

720 # working_dist range 

721 min_wd_mm = selected_beam.working_distance.limits.min * Conversions.M_TO_MM 

722 max_wd_mm = selected_beam.working_distance.limits.max * Conversions.M_TO_MM 

723 

724 return tbt.BeamLimits( 

725 voltage_kv=tbt.Limit(min=min_kv, max=max_kv), 

726 current_na=tbt.Limit(min=min_na, max=max_na), 

727 hfw_mm=tbt.Limit(min=min_hfw_mm, max=max_hfw_mm), 

728 working_distance_mm=tbt.Limit(min=min_wd_mm, max=max_wd_mm), 

729 ) 

730 

731 

732def general( 

733 general_db: dict, 

734 yml_format: tbt.YMLFormatVersion, 

735) -> tbt.GeneralSettings: 

736 """ 

737 Convert a general settings dictionary to a built-in type and perform schema checking. 

738 

739 This function converts a general settings dictionary from a .yml file to a `GeneralSettings` object. It performs schema checking to ensure valid inputs are requested. 

740 

741 ## Parameters 

742 

743 - `general_db` (`dict`): The general settings dictionary from the .yml file. 

744 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

745 

746 ## Returns 

747 

748 - `tbt.GeneralSettings`: The general settings object. 

749 

750 ## Raises 

751 

752 - `NotImplementedError`: If the provided yml format is unsupported. 

753 """ 

754 

755 if not yml_format in tbt.YMLFormatVersion: 

756 raise NotImplementedError( 

757 """Due to the complexity and number of variables, 

758 image objects should only be constructed using a yml file.""" 

759 ) 

760 if not isinstance(yml_format, tbt.YMLFormatVersion): 

761 raise NotImplementedError(f"Unsupported yml format of {yml_format}.") 

762 

763 validate_general_settings( 

764 settings=general_db, 

765 yml_format=yml_format, 

766 ) 

767 

768 if yml_format.version >= 1.0: 

769 # slice thickness 

770 slice_thickness_um = general_db["slice_thickness_um"] 

771 # max slice number 

772 max_slice_number = general_db["max_slice_num"] 

773 # pre tilt 

774 pre_tilt_deg = general_db["pre_tilt_deg"] 

775 # sectioning axis 

776 sectioning_axis = tbt.SectioningAxis(general_db["sectioning_axis"]) 

777 # stage tolerance 

778 stage_tolerance = tbt.StageTolerance( 

779 translational_um=general_db["stage_translational_tol_um"], 

780 angular_deg=general_db["stage_angular_tol_deg"], 

781 ) 

782 # connection 

783 connection = tbt.MicroscopeConnection( 

784 general_db["connection_host"], general_db["connection_port"] 

785 ) 

786 # EBSD OEM 

787 ebsd_oem = tbt.ExternalDeviceOEM(general_db["EBSD_OEM"]) 

788 # EDS OEM 

789 eds_oem = tbt.ExternalDeviceOEM(general_db["EDS_OEM"]) 

790 # exp dir 

791 exp_dir = general_db["exp_dir"] 

792 # h5 log name 

793 h5_log_name = general_db["h5_log_name"] 

794 # remove log file extension if the user provided it 

795 log_extension = Constants.logfile_extension 

796 if h5_log_name.endswith(log_extension): 796 ↛ 799line 796 didn't jump to line 799 because the condition on line 796 was always true

797 h5_log_name = h5_log_name[: -len(log_extension)] 

798 # step count 

799 step_count = general_db["step_count"] 

800 yml_version = 1.0 

801 

802 general_settings = tbt.GeneralSettings( 

803 yml_version=yml_version, 

804 slice_thickness_um=slice_thickness_um, 

805 max_slice_number=max_slice_number, 

806 pre_tilt_deg=pre_tilt_deg, 

807 sectioning_axis=sectioning_axis, 

808 stage_tolerance=stage_tolerance, 

809 connection=connection, 

810 EBSD_OEM=ebsd_oem, 

811 EDS_OEM=eds_oem, 

812 exp_dir=Path(exp_dir), 

813 h5_log_name=h5_log_name, 

814 step_count=step_count, 

815 ) 

816 

817 return general_settings 

818 

819 

820def laser_box_pattern(settings: dict) -> tbt.LaserBoxPattern: 

821 """ 

822 Convert a dictionary of laser box pattern settings to a `LaserBoxPattern` object. 

823 

824 This function takes a dictionary of laser box pattern settings and converts it to a `LaserBoxPattern` object. 

825 

826 ## Parameters 

827 

828 - `settings` (`dict`): The dictionary containing laser box pattern settings. 

829 

830 ## Returns 

831 

832 - `tbt.LaserBoxPattern`: The laser box pattern object. 

833 """ 

834 return tbt.LaserBoxPattern( 

835 passes=settings["passes"], 

836 size_x_um=settings["size_x_um"], 

837 size_y_um=settings["size_y_um"], 

838 pitch_x_um=settings["pitch_x_um"], 

839 pitch_y_um=settings["pitch_y_um"], 

840 scan_type=tbt.LaserScanType(settings["scan_type"]), 

841 coordinate_ref=tbt.CoordinateReference(settings["coordinate_ref"]), 

842 ) 

843 

844 

845def laser_line_pattern(settings: dict) -> tbt.LaserBoxPattern: 

846 """ 

847 Convert a dictionary of laser line pattern settings to a `LaserLinePattern` object. 

848 

849 This function takes a dictionary of laser line pattern settings and converts it to a `LaserLinePattern` object. 

850 

851 ## Parameters 

852 

853 - `settings` (`dict`): The dictionary containing laser line pattern settings. 

854 

855 ## Returns 

856 

857 - `tbt.LaserLinePattern`: The laser line pattern object. 

858 """ 

859 return tbt.LaserLinePattern( 

860 passes=settings["passes"], 

861 size_um=settings["size_um"], 

862 pitch_um=settings["pitch_um"], 

863 scan_type=tbt.LaserScanType(settings["scan_type"]), 

864 ) 

865 

866 

867def laser( 

868 microscope: tbt.Microscope, 

869 step_settings: dict, 

870 step_name: str, 

871 yml_format: tbt.YMLFormatVersion, 

872) -> tbt.LaserSettings: 

873 """ 

874 Convert a laser step from a .yml file to microscope settings for performing laser milling. 

875 

876 This function converts a laser step from a .yml file to `LaserSettings` for the microscope. It performs schema checking to ensure valid inputs are requested. 

877 

878 ## Parameters 

879 

880 - `microscope` (`tbt.Microscope`): The microscope object for which to set the laser settings. 

881 - `step_settings` (`dict`): The dictionary containing the laser step settings from the .yml file. 

882 - `step_name` (`str`): The name of the step in the .yml file. 

883 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

884 

885 ## Returns 

886 

887 - `tbt.LaserSettings`: The laser settings object. 

888 

889 ## Raises 

890 

891 - `KeyError`: If required settings are missing from the .yml file. 

892 """ 

893 if yml_format.version >= 1.0: 

894 # pulse settings 

895 pulse_set_db = step_settings.get("pulse") 

896 if pulse_set_db is None: 

897 raise KeyError( 

898 f"Invalid .yml file, no 'pulse' settings found in 'laser' step_type for step '{step_name}'." 

899 ) 

900 # laser optics settings 

901 optics_set_db = { 

902 "objective_position_mm": step_settings.get("objective_position_mm"), 

903 "beam_shift_um_x": step_settings.get("beam_shift").get("x_um"), 

904 "beam_shift_um_y": step_settings.get("beam_shift").get("y_um"), 

905 } 

906 # pattern settings 

907 pattern_set_db = step_settings.get("pattern") 

908 if pattern_set_db is None: 908 ↛ 912line 908 didn't jump to line 912 because the condition on line 908 was always true

909 raise KeyError( 

910 f"Invalid .yml file, no 'pattern' settings found in 'laser' step_type for step '{step_name}'." 

911 ) 

912 line_pattern_db = pattern_set_db.get("type").get("line") 

913 box_pattern_db = pattern_set_db.get("type").get("box") 

914 

915 validate_pulse_settings( 

916 settings=pulse_set_db, 

917 yml_format=yml_format, 

918 step_name=step_name, 

919 ) 

920 pulse = tbt.LaserPulse( 

921 wavelength_nm=tbt.LaserWavelength(pulse_set_db["wavelength_nm"]), 

922 divider=pulse_set_db["divider"], 

923 energy_uj=pulse_set_db["energy_uj"], 

924 polarization=tbt.LaserPolarization(pulse_set_db["polarization"]), 

925 ) 

926 

927 validate_laser_optics_settings( 

928 settings=optics_set_db, 

929 yml_format=yml_format, 

930 step_name=step_name, 

931 ) 

932 

933 pattern_type = validate_laser_pattern_settings( 

934 settings=pattern_set_db, 

935 yml_format=yml_format, 

936 step_name=step_name, 

937 ) 

938 if pattern_type == tbt.LaserPatternType.BOX: 938 ↛ 940line 938 didn't jump to line 940 because the condition on line 938 was always true

939 geometry = laser_box_pattern(box_pattern_db) 

940 if pattern_type == tbt.LaserPatternType.LINE: 940 ↛ 942line 940 didn't jump to line 942 because the condition on line 940 was always true

941 geometry = laser_line_pattern(line_pattern_db) 

942 pattern = tbt.LaserPattern( 

943 mode=tbt.LaserPatternMode(pattern_set_db["mode"]), 

944 rotation_deg=pattern_set_db["rotation_deg"], 

945 pulses_per_pixel=pattern_set_db["pulses_per_pixel"], 

946 pixel_dwell_ms=pattern_set_db["pixel_dwell_ms"], 

947 geometry=geometry, 

948 ) 

949 

950 laser_settings = tbt.LaserSettings( 

951 microscope=microscope, 

952 pulse=pulse, 

953 objective_position_mm=optics_set_db["objective_position_mm"], 

954 beam_shift_um=tbt.Point( 

955 x=optics_set_db["beam_shift_um_x"], 

956 y=optics_set_db["beam_shift_um_y"], 

957 ), 

958 pattern=pattern, 

959 ) 

960 

961 return laser_settings 

962 

963 

964def image( 

965 microscope: tbt.Microscope, 

966 step_settings: dict, 

967 step_name: str, 

968 yml_format: tbt.YMLFormatVersion, 

969) -> tbt.ImageSettings: 

970 """ 

971 Convert an image step from a .yml file to microscope settings for capturing an image. 

972 

973 This function converts an image step from a .yml file to `ImageSettings` for the microscope. It performs schema checking to ensure valid inputs are requested. 

974 

975 ## Parameters 

976 

977 - `microscope` (`tbt.Microscope`): The microscope object for which to set the image settings. 

978 - `step_settings` (`dict`): The dictionary containing the image step settings from the .yml file. 

979 - `step_name` (`str`): The name of the step in the .yml file. 

980 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

981 

982 ## Returns 

983 

984 - `tbt.ImageSettings`: The image settings object. 

985 

986 ## Raises 

987 

988 - `KeyError`: If required settings are missing from the .yml file. 

989 - `NotImplementedError`: If the provided beam type is unsupported. 

990 - `ValueError`: If invalid scan rotation is requested with dynamic focus or tilt correction, or if the bit depth is unsupported. 

991 """ 

992 if yml_format.version >= 1.0: 992 ↛ 993,   992 ↛ 10362 missed branches: 1) line 992 didn't jump to line 993 because the condition on line 992 was never true, 2) line 992 didn't jump to line 1036 because the condition on line 992 was always true

993 step_general = step_settings.get(yml_format.step_general_key) 

994 if step_general is None: 

995 raise KeyError( 

996 f"Invalid .yml file, no 'step_general' settings found in step '{step_name}'." 

997 ) 

998 step_type = step_general.get(yml_format.step_type_key) 

999 if step_type is None: 

1000 raise KeyError( 

1001 f"Invalid .yml file, no 'step_type' settings found in step '{step_name}'." 

1002 ) 

1003 # beam settings 

1004 beam_set_db = step_settings.get("beam") 

1005 if beam_set_db is None: 1005 ↛ 1006line 1005 didn't jump to line 1006 because the condition on line 1005 was never true

1006 raise KeyError( 

1007 f"Invalid .yml file, no 'beam' settings found in '{step_type}' step_type for step '{step_name}'." 

1008 ) 

1009 beam_type_value = beam_set_db.get("type") 

1010 if not ut.valid_enum_entry(beam_type_value, tbt.BeamType): 

1011 raise NotImplementedError( 

1012 f"Unsupported beam type of '{beam_type_value}', supported beam types are: {[i.value for i in tbt.BeamType]}." 

1013 ) 

1014 beam_type = tbt.BeamType(beam_set_db.get("type")) 

1015 

1016 # detector settings 

1017 detector_set_db = step_settings.get("detector") 

1018 if detector_set_db is None: 

1019 raise KeyError( 

1020 f"Invalid .yml file, no 'detector' settings found in '{step_type}' step_type for step '{step_name}'." 

1021 ) 

1022 auto_cb_set_db = detector_set_db.get("auto_cb") 

1023 

1024 # scan settings 

1025 scan_set_db = step_settings.get("scan") 

1026 if scan_set_db is None: 

1027 raise KeyError( 

1028 f"Invalid .yml file, no 'scan' settings found in '{step_type}' step_type for step '{step_name}'." 

1029 ) 

1030 scan_res = string_to_res(scan_set_db.get("resolution")) 

1031 

1032 # misc settings 

1033 bit_depth = step_settings.get("bit_depth") 

1034 

1035 # TODO incorporate tile settings 

1036 if yml_format.version >= 1.1: 1036 ↛ 1038,   1036 ↛ 10402 missed branches: 1) line 1036 didn't jump to line 1038 because the condition on line 1036 was never true, 2) line 1036 didn't jump to line 1040 because the condition on line 1036 was always true

1037 # tile settings 

1038 tile_set_db = step_settings.get("tile_settings") 

1039 

1040 validate_beam_settings( 

1041 microscope=microscope, 

1042 beam_type=beam_type, 

1043 settings=beam_set_db, 

1044 yml_format=yml_format, 

1045 step_name=step_name, 

1046 ) 

1047 beam_settings = tbt.BeamSettings( 

1048 voltage_kv=beam_set_db["voltage_kv"], 

1049 voltage_tol_kv=beam_set_db["voltage_tol_kv"], 

1050 current_na=beam_set_db["current_na"], 

1051 current_tol_na=beam_set_db["current_tol_na"], 

1052 hfw_mm=beam_set_db["hfw_mm"], 

1053 working_dist_mm=beam_set_db["working_dist_mm"], 

1054 dynamic_focus=beam_set_db["dynamic_focus"], 

1055 tilt_correction=beam_set_db["tilt_correction"], 

1056 ) 

1057 

1058 validate_auto_cb_settings( 

1059 settings=auto_cb_set_db, 

1060 yml_format=yml_format, 

1061 step_name=step_name, 

1062 ) 

1063 auto_cb_settings = tbt.ScanArea( 

1064 left=auto_cb_set_db["left"], 

1065 top=auto_cb_set_db["top"], 

1066 width=auto_cb_set_db["width"], 

1067 height=auto_cb_set_db["height"], 

1068 ) 

1069 

1070 validate_detector_settings( 

1071 microscope=microscope, 

1072 beam_type=beam_type, 

1073 settings=detector_set_db, 

1074 yml_format=yml_format, 

1075 step_name=step_name, 

1076 ) 

1077 detector_settings = tbt.Detector( 

1078 type=tbt.DetectorType(detector_set_db["type"]), 

1079 mode=tbt.DetectorMode(detector_set_db["mode"]), 

1080 brightness=detector_set_db["brightness"], 

1081 contrast=detector_set_db["contrast"], 

1082 auto_cb_settings=auto_cb_settings, 

1083 # custom_settings=None, 

1084 ) 

1085 

1086 validate_scan_settings( 

1087 microscope=microscope, 

1088 beam_type=beam_type, 

1089 settings=scan_set_db, 

1090 yml_format=yml_format, 

1091 step_name=step_name, 

1092 ) 

1093 

1094 # cast resolution to preset if applicable 

1095 if ut.valid_enum_entry(obj=scan_res, check_type=tbt.PresetResolution): 

1096 scan_res = tbt.PresetResolution(scan_res) 

1097 

1098 scan_settings = tbt.Scan( 

1099 rotation_deg=scan_set_db["rotation_deg"], 

1100 dwell_time_us=scan_set_db["dwell_time_us"], 

1101 resolution=scan_res, 

1102 # mode=tbt.ScanMode(scan_set_db["mode"]), 

1103 ) 

1104 

1105 # make sure Scan rotation is 0 if dynamic focus or tilt correction is on (using auto mode only for angular correction) 

1106 if beam_settings.dynamic_focus or beam_settings.tilt_correction: 1106 ↛ 1113line 1106 didn't jump to line 1113 because the condition on line 1106 was always true

1107 if not math.isclose(a=scan_settings.rotation_deg, b=0.0): 1107 ↛ 1113line 1107 didn't jump to line 1113 because the condition on line 1107 was always true

1108 raise ValueError( 

1109 f"Invalid .yml for step '{step_name}'. Scan rotation of '{scan_settings.rotation_deg}' degrees requested with tilt_correction and/or dynamic focus set to 'True'. Cannot use dynamic focus or tilt correction for non-zero scan rotation." 

1110 ) 

1111 

1112 # validate bit_depth 

1113 if not ut.valid_enum_entry(bit_depth, tbt.ColorDepth): 

1114 valid_bit_depths = [i.value for i in tbt.ColorDepth] 1114 ↛ exit,   1114 ↛ 11152 missed branches: 1) line 1114 didn't run the list comprehension on line 1114, 2) line 1114 didn't jump to line 1115 because

1115 raise ValueError( 

1116 f"Unsupported bit depth of {bit_depth}, available depths are {valid_bit_depths}" 

1117 ) 

1118 

1119 image_settings = tbt.ImageSettings( 

1120 microscope=microscope, 

1121 beam=beam_object_type(beam_type)(settings=beam_settings), 

1122 detector=detector_settings, 

1123 scan=scan_settings, 

1124 bit_depth=bit_depth, 

1125 ) 

1126 

1127 return image_settings 

1128 

1129 

1130def fib( 

1131 microscope: tbt.Microscope, 

1132 step_settings: dict, 

1133 step_name: str, 

1134 yml_format: tbt.YMLFormatVersion, 

1135) -> tbt.FIBSettings: 

1136 """ 

1137 Convert a FIB step from a .yml file to microscope settings for performing a FIB operation. 

1138 

1139 This function converts a FIB step from a .yml file to `FIBSettings` for the microscope. It performs schema checking to ensure valid inputs are requested. 

1140 

1141 ## Parameters 

1142 

1143 - `microscope` (`tbt.Microscope`): The microscope object for which to set the FIB settings. 

1144 - `step_settings` (`dict`): The dictionary containing the FIB step settings from the .yml file. 

1145 - `step_name` (`str`): The name of the step in the .yml file. 

1146 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1147 

1148 ## Returns 

1149 

1150 - `tbt.FIBSettings`: The FIB settings object. 

1151 

1152 ## Raises 

1153 

1154 - `KeyError`: If required settings are missing from the .yml file. 

1155 - `ValueError`: If invalid beam type is requested. 

1156 """ 

1157 ## create image_step_settings from this 

1158 if yml_format.version >= 1.0: 1158 ↛ 1159,   1158 ↛ 11672 missed branches: 1) line 1158 didn't jump to line 1159 because the condition on line 1158 was never true, 2) line 1158 didn't jump to line 1167 because the condition on line 1158 was always true

1159 image_step_settings = step_settings.get("image") 

1160 image_step_settings["step_general"] = step_settings.get("step_general") 

1161 

1162 mill_step_settings = step_settings.get("mill") 

1163 mill_beam_db = mill_step_settings.get("beam") 

1164 mill_pattern_db = mill_step_settings.get("pattern") 

1165 

1166 # ensure image is with an ion beam 

1167 enforce_beam_type( 

1168 tbt.IonBeam(settings=None), 

1169 step_settings=image_step_settings, 

1170 step_name=step_name, 

1171 yml_format=yml_format, 

1172 ) 

1173 image_settings = image( 

1174 microscope=microscope, 

1175 step_settings=image_step_settings, 

1176 step_name=step_name, 

1177 yml_format=yml_format, 

1178 ) 

1179 

1180 ## fib mill settings 

1181 # ensure milling is with an ion beam 

1182 enforce_beam_type( 

1183 tbt.IonBeam(settings=None), 

1184 step_settings=mill_step_settings, 

1185 step_name=step_name, 

1186 yml_format=yml_format, 

1187 ) 

1188 beam_type = tbt.BeamType(mill_beam_db.get("type")) 

1189 # mill beam 

1190 validate_beam_settings( 

1191 microscope=microscope, 

1192 beam_type=beam_type, 

1193 settings=mill_beam_db, 

1194 yml_format=yml_format, 

1195 step_name=step_name, 

1196 ) 

1197 mill_beam = tbt.IonBeam( 

1198 settings=tbt.BeamSettings( 

1199 voltage_kv=mill_beam_db["voltage_kv"], 

1200 voltage_tol_kv=mill_beam_db["voltage_tol_kv"], 

1201 current_na=mill_beam_db["current_na"], 

1202 current_tol_na=mill_beam_db["current_tol_na"], 

1203 hfw_mm=mill_beam_db["hfw_mm"], 

1204 working_dist_mm=mill_beam_db["working_dist_mm"], 

1205 dynamic_focus=mill_beam_db["dynamic_focus"], 

1206 tilt_correction=mill_beam_db["tilt_correction"], 

1207 ) 

1208 ) 

1209 

1210 # fib pattern settings 

1211 pattern = validate_fib_pattern_settings( 

1212 microscope=microscope, 

1213 settings=mill_pattern_db, 

1214 yml_format=yml_format, 

1215 step_name=step_name, 

1216 ) 

1217 

1218 fib_settings = tbt.FIBSettings( 

1219 microscope=microscope, 

1220 image=image_settings, 

1221 mill_beam=mill_beam, 

1222 pattern=pattern, 

1223 ) 

1224 return fib_settings 

1225 

1226 

1227@singledispatch 

1228def enforce_beam_type( 

1229 beam_type, 

1230 step_settings: dict, 

1231 step_name: str, 

1232 yml_format: tbt.YMLFormatVersion, 

1233) -> bool: 

1234 """ 

1235 Enforce a specific beam type is used for an operation based on a dictionary. 

1236 

1237 This function ensures that the specified beam type is used for an operation based on the provided settings dictionary. The dictionary must contain a sub-dictionary with the key 'beam'. 

1238 

1239 ## Parameters 

1240 

1241 - `beam_type` (`Any`): The beam type to enforce. 

1242 - `step_settings` (`dict`): The dictionary containing the step settings. 

1243 - `step_name` (`str`): The name of the step in the .yml file. 

1244 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1245 

1246 ## Returns 

1247 

1248 - `bool`: True if the beam type is enforced successfully. 

1249 

1250 ## Raises 

1251 

1252 - `NotImplementedError`: If no handler is available for the provided type. 

1253 """ 

1254 _ = beam_type 

1255 __ = step_settings 

1256 ___ = step_name 

1257 ____ = yml_format 

1258 raise NotImplementedError(f"No handler for type {type(step_settings)}") 

1259 

1260 

1261@enforce_beam_type.register 

1262def _( 

1263 beam_type: tbt.ElectronBeam, 

1264 step_settings: dict, 

1265 step_name: str, 

1266 yml_format: tbt.YMLFormatVersion, 

1267) -> bool: 

1268 """ 

1269 Enforce that an electron beam is used for an operation based on a dictionary. 

1270 

1271 This function ensures that an electron beam is used for an operation based on the provided settings dictionary. 

1272 

1273 ## Parameters 

1274 

1275 - `beam_type` (`tbt.ElectronBeam`): The electron beam type to enforce. 

1276 - `step_settings` (`dict`): The dictionary containing the step settings. 

1277 - `step_name` (`str`): The name of the step in the .yml file. 

1278 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1279 

1280 ## Returns 

1281 

1282 - `bool`: True if the electron beam type is enforced successfully. 

1283 

1284 ## Raises 

1285 

1286 - `KeyError`: If the 'beam' settings are missing from the .yml file. 

1287 - `NotImplementedError`: If the beam type is unsupported or not an electron beam. 

1288 """ 

1289 # beam must be electron 

1290 if yml_format.version >= 1.0: 

1291 beam_set_db = step_settings.get("beam") 

1292 if beam_set_db is None: 1292 ↛ 1296line 1292 didn't jump to line 1296 because the condition on line 1292 was always true

1293 raise KeyError( 

1294 f"Invalid .yml file, no 'beam' settings found in step '{step_name}'." 

1295 ) 

1296 beam_type_value = beam_set_db.get("type") 

1297 

1298 electron_beam_error_message = f"Unsupported beam type of '{beam_type_value}' in step '{step_name}'. '{tbt.BeamType.ELECTRON.value}' beam type must be used." 

1299 if not ut.valid_enum_entry(beam_type_value, tbt.BeamType): 

1300 raise NotImplementedError(electron_beam_error_message) 

1301 if not tbt.BeamType(beam_type_value) == tbt.BeamType.ELECTRON: 

1302 raise NotImplementedError(electron_beam_error_message) 

1303 

1304 

1305@enforce_beam_type.register 

1306def _( 

1307 beam_type: tbt.IonBeam, 

1308 step_settings: dict, 

1309 step_name: str, 

1310 yml_format: tbt.YMLFormatVersion, 

1311) -> bool: 

1312 """ 

1313 Enforce that an ion beam is used for an operation based on a dictionary. 

1314 

1315 This function ensures that an ion beam is used for an operation based on the provided settings dictionary. 

1316 

1317 ## Parameters 

1318 

1319 - `beam_type` (`tbt.IonBeam`): The ion beam type to enforce. 

1320 - `step_settings` (`dict`): The dictionary containing the step settings. 

1321 - `step_name` (`str`): The name of the step in the .yml file. 

1322 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1323 

1324 ## Returns 

1325 

1326 - `bool`: True if the ion beam type is enforced successfully. 

1327 

1328 ## Raises 

1329 

1330 - `KeyError`: If the 'beam' settings are missing from the .yml file. 

1331 - `NotImplementedError`: If the beam type is unsupported or not an ion beam. 

1332 """ 

1333 # beam must be ion 

1334 if yml_format.version >= 1.0: 

1335 beam_set_db = step_settings.get("beam") 

1336 if beam_set_db is None: 1336 ↛ 1337,   1336 ↛ 13402 missed branches: 1) line 1336 didn't jump to line 1337 because the condition on line 1336 was never true, 2) line 1336 didn't jump to line 1340 because the condition on line 1336 was always true

1337 raise KeyError( 

1338 f"Invalid .yml file, no 'beam' settings found in step '{step_name}'." 

1339 ) 

1340 beam_type_value = beam_set_db.get("type") 

1341 

1342 ion_beam_error_message = f"Unsupported beam type of '{beam_type_value}' for step '{step_name}'. '{tbt.BeamType.ION.value}' beam type must be used." 

1343 if not ut.valid_enum_entry(beam_type_value, tbt.BeamType): 

1344 raise NotImplementedError(ion_beam_error_message) 

1345 if not tbt.BeamType(beam_type_value) == tbt.BeamType.ION: 

1346 raise NotImplementedError(ion_beam_error_message) 

1347 

1348 

1349def ebsd( 

1350 microscope: tbt.Microscope, 

1351 step_settings: dict, 

1352 step_name: str, 

1353 yml_format: tbt.YMLFormatVersion, 

1354) -> tbt.EBSDSettings: 

1355 """ 

1356 Convert an EBSD step from a .yml file to microscope settings for performing an EBSD operation. 

1357 

1358 This function converts an EBSD step from a .yml file to `EBSDSettings` for the microscope. It performs schema checking to ensure valid inputs are requested. 

1359 

1360 ## Parameters 

1361 

1362 - `microscope` (`tbt.Microscope`): The microscope object for which to set the EBSD settings. 

1363 - `step_settings` (`dict`): The dictionary containing the EBSD step settings from the .yml file. 

1364 - `step_name` (`str`): The name of the step in the .yml file. 

1365 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1366 

1367 ## Returns 

1368 

1369 - `tbt.EBSDSettings`: The EBSD settings object. 

1370 

1371 ## Raises 

1372 

1373 - `KeyError`: If required settings are missing from the .yml file or if the 'concurrent_EDS' key is invalid. 

1374 """ 

1375 enforce_beam_type( 

1376 tbt.ElectronBeam(settings=None), 

1377 step_settings=step_settings, 

1378 step_name=step_name, 

1379 yml_format=yml_format, 

1380 ) 

1381 image_settings = image( 

1382 microscope=microscope, 

1383 step_settings=step_settings, 

1384 step_name=step_name, 

1385 yml_format=yml_format, 

1386 ) 

1387 concurrent_EDS = step_settings.get("concurrent_EDS") 

1388 if (concurrent_EDS is None) or (not concurrent_EDS): 

1389 enable_eds = False 

1390 elif concurrent_EDS == True: 

1391 enable_eds = True 

1392 else: 

1393 raise KeyError( 

1394 f"Invalid .yml file, for step '{step_name}', an EBSD type step. 'concurrent_EDS' key is '{concurrent_EDS}' of type {type(concurrent_EDS)} but must be boolean (True/False) or of NoneType (null in .yml)." 

1395 ) 

1396 

1397 ebsd_settings = tbt.EBSDSettings( 

1398 image=image_settings, 

1399 enable_eds=enable_eds, 

1400 enable_ebsd=True, 

1401 ) 

1402 return ebsd_settings 

1403 

1404 

1405def eds( 

1406 microscope: tbt.Microscope, 

1407 step_settings: dict, 

1408 step_name: str, 

1409 yml_format: tbt.YMLFormatVersion, 

1410) -> tbt.EDSSettings: 

1411 """ 

1412 Convert an EDS step from a .yml file to microscope settings for performing an EDS operation. 

1413 

1414 This function converts an EDS step from a .yml file to `EDSSettings` for the microscope. It performs schema checking to ensure valid inputs are requested. 

1415 

1416 ## Parameters 

1417 

1418 - `microscope` (`tbt.Microscope`): The microscope object for which to set the EDS settings. 

1419 - `step_settings` (`dict`): The dictionary containing the EDS step settings from the .yml file. 

1420 - `step_name` (`str`): The name of the step in the .yml file. 

1421 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1422 

1423 ## Returns 

1424 

1425 - `tbt.EDSSettings`: The EDS settings object. 

1426 """ 

1427 enforce_beam_type( 

1428 tbt.ElectronBeam(settings=None), 

1429 step_settings=step_settings, 

1430 step_name=step_name, 

1431 yml_format=yml_format, 

1432 ) 

1433 image_settings = image( 

1434 microscope=microscope, 

1435 step_settings=step_settings, 

1436 step_name=step_name, 

1437 yml_format=yml_format, 

1438 ) 

1439 eds_settings = tbt.EDSSettings( 

1440 image=image_settings, 

1441 enable_eds=True, 

1442 ) 

1443 return eds_settings 

1444 

1445 

1446def custom( 

1447 microscope: tbt.Microscope, 

1448 step_settings: dict, 

1449 step_name: str, 

1450 yml_format: tbt.YMLFormatVersion, 

1451) -> tbt.CustomSettings: 

1452 """ 

1453 Convert a custom step from a .yml file to custom settings for the microscope. 

1454 

1455 This function converts a custom step from a .yml file to `CustomSettings` for the microscope. It performs schema checking to ensure valid inputs are requested. 

1456 

1457 ## Parameters 

1458 

1459 - `microscope` (`tbt.Microscope`): The microscope object for which to set the custom settings. 

1460 - `step_settings` (`dict`): The dictionary containing the custom step settings from the .yml file. 

1461 - `step_name` (`str`): The name of the step in the .yml file. 

1462 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1463 

1464 ## Returns 

1465 

1466 - `tbt.CustomSettings`: The custom settings object. 

1467 

1468 ## Raises 

1469 

1470 - `KeyError`: If required settings are missing from the .yml file. 

1471 - `ValueError`: If the specified script or executable path does not exist. 

1472 """ 

1473 if yml_format.version >= 1.0: 

1474 script_path = step_settings.get("script_path") 

1475 if script_path is None: 

1476 raise KeyError( 

1477 f"Invalid .yml file, no 'script_path' found in custom step_type for step '{step_name}'." 

1478 ) 

1479 if not Path(script_path).is_file(): 

1480 raise ValueError( 

1481 f"Invalid location for script at location {script_path}. File does not exist." 

1482 ) 

1483 

1484 executable_path = step_settings.get("executable_path") 

1485 if executable_path is None: 1485 ↛ 1486,   1485 ↛ 14892 missed branches: 1) line 1485 didn't jump to line 1486 because the condition on line 1485 was never true, 2) line 1485 didn't jump to line 1489 because the condition on line 1485 was always true

1486 raise KeyError( 

1487 f"Invalid .yml file, no 'executable_path' found in custom step_type for step '{step_name}'." 

1488 ) 

1489 if not Path(executable_path).is_file(): 

1490 raise ValueError( 

1491 f"Invalid location for executable at location {executable_path}. File does not exist." 

1492 ) 

1493 

1494 custom_settings = tbt.CustomSettings( 

1495 script_path=Path(script_path), 

1496 executable_path=Path(executable_path), 

1497 ) 

1498 return custom_settings 

1499 

1500 

1501# def fib_pattern_type( 

1502# settings: tbt.FIBSettings, 

1503# ) -> Union[tbt.FIBBoxPattern, tbt.FIBStreamPattern]: 

1504# """Returns specific pattern type settings for a properly formatted FIBSettings object""" 

1505 

1506 

1507def scan_limits( 

1508 selected_beam: property, 

1509) -> tbt.ScanLimits: 

1510 """ 

1511 Retrieve the scan settings limits for the selected beam. 

1512 

1513 This function retrieves the limits for rotation and dwell time for the selected beam and returns them as a `ScanLimits` object. 

1514 

1515 ## Parameters 

1516 

1517 - `selected_beam` (`property`): The selected beam property from which to retrieve the scan limits. 

1518 

1519 ## Returns 

1520 

1521 - `tbt.ScanLimits`: The scan limits for rotation (degrees) and dwell time (microseconds). 

1522 """ 

1523 # rotation 

1524 min_deg = selected_beam.scanning.rotation.limits.min * Conversions.RAD_TO_DEG 

1525 max_deg = selected_beam.scanning.rotation.limits.max * Conversions.RAD_TO_DEG 

1526 # dwell_time 

1527 min_dwell_us = selected_beam.scanning.dwell_time.limits.min * Conversions.S_TO_US 

1528 max_dwell_us = selected_beam.scanning.dwell_time.limits.max * Conversions.S_TO_US 

1529 

1530 return tbt.ScanLimits( 

1531 rotation_deg=tbt.Limit(min=min_deg, max=max_deg), 

1532 dwell_us=tbt.Limit(min=min_dwell_us, max=max_dwell_us), 

1533 ) 

1534 

1535 

1536def string_to_res(input: str) -> tbt.Resolution: 

1537 """ 

1538 Convert a string in the format "{{width}}x{{height}}" to a resolution object. 

1539 

1540 This function takes a string representing the resolution in the format "WIDTHxHEIGHT" and converts it to a `Resolution` object. 

1541 

1542 ## Parameters 

1543 

1544 - `input` (`str`): The string representing the resolution in the format "WIDTHxHEIGHT". 

1545 

1546 ## Returns 

1547 

1548 - `tbt.Resolution`: The resolution object. 

1549 

1550 ## Raises 

1551 

1552 - `ValueError`: If the input string is not in the expected format. 

1553 """ 

1554 try: 

1555 split_res = (input.lower()).split("x") 

1556 width, height = int(split_res[0]), int(split_res[1]) 

1557 except: 

1558 raise ValueError( 

1559 f"""Invalid string format, 

1560 expected string format of "WIDTHxHEIGHT", 

1561 but received the following: "{input}".""" 

1562 ) 

1563 

1564 return tbt.Resolution(width=width, height=height) 

1565 

1566 

1567def valid_string_resolution(string_resolution: str) -> bool: 

1568 """ 

1569 Validate a string resolution. 

1570 

1571 This function validates a string resolution by converting it to a `Resolution` object and checking if the width and height are within the specified limits. 

1572 

1573 ## Parameters 

1574 

1575 - `string_resolution` (`str`): The string representing the resolution in the format "WIDTHxHEIGHT". 

1576 

1577 ## Returns 

1578 

1579 - `bool`: True if the resolution is valid, False otherwise. 

1580 """ 

1581 res = string_to_res(string_resolution) 

1582 width, height = res.width, res.height 

1583 return ( 

1584 ut.in_interval( 

1585 width, 

1586 limit=Constants.scan_resolution_limit, 

1587 type=tbt.IntervalType.CLOSED, 

1588 ) 

1589 ) and ( 

1590 ut.in_interval( 

1591 height, 

1592 limit=Constants.scan_resolution_limit, 

1593 type=tbt.IntervalType.CLOSED, 

1594 ) 

1595 ) 

1596 

1597 

1598def validate_auto_cb_settings( 

1599 settings: dict, 

1600 yml_format: tbt.YMLFormatVersion, 

1601 step_name: str, 

1602) -> bool: 

1603 """ 

1604 Perform schema checking for auto contrast/brightness setting dictionary. 

1605 

1606 This function validates the auto contrast/brightness settings dictionary based on the specified yml format. 

1607 

1608 ## Parameters 

1609 

1610 - `settings` (`dict`): The dictionary containing the auto contrast/brightness settings. 

1611 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1612 - `step_name` (`str`): The name of the step in the .yml file. 

1613 

1614 ## Returns 

1615 

1616 - `bool`: True if the settings are valid, False otherwise. 

1617 

1618 ## Raises 

1619 

1620 - `KeyError`: If required keys are missing from the settings dictionary. 

1621 - `ValueError`: If the settings do not satisfy the specified schema. 

1622 """ 

1623 

1624 if ut.none_value_dictionary(settings): 1624 ↛ 1627line 1624 didn't jump to line 1627 because the condition on line 1624 was always true

1625 return True 

1626 

1627 if settings.get("left") is None or settings.get("top") is None: 1627 ↛ 1636line 1627 didn't jump to line 1636 because the condition on line 1627 was always true

1628 if settings.get("left") is None: 1628 ↛ 1629,   1628 ↛ 16312 missed branches: 1) line 1628 didn't jump to line 1629 because the condition on line 1628 was never true, 2) line 1628 didn't jump to line 1631 because the condition on line 1628 was always true

1629 missing_key = "left" 

1630 else: 

1631 missing_key = "top" 

1632 raise KeyError( 

1633 f"Missing or no value for '{missing_key}' key in 'auto_cb' sub-dictionary in step '{step_name}' All 'auto_cb' sub-dictionary values must be declared by the user to implement this capability." 

1634 ) 

1635 

1636 origin_limit = tbt.Limit(min=0.0, max=1.0) # reduced area limit for scan window 

1637 width_limit = tbt.Limit(min=0.0, max=1.0 - settings["left"]) 

1638 height_limit = tbt.Limit(min=0.0, max=1.0 - settings["top"]) 

1639 

1640 if yml_format.version >= 1.0: 1640 ↛ 1683line 1640 didn't jump to line 1683 because the condition on line 1640 was always true

1641 schema = Schema( 1641 ↛ 1683line 1641 didn't jump to line 1683 because

1642 { 

1643 "left": And( 

1644 float, 

1645 lambda x: ut.in_interval( 

1646 x, 

1647 limit=origin_limit, 

1648 type=tbt.IntervalType.RIGHT_OPEN, 

1649 ), 

1650 error=f"In step '{step_name}', requested auto contrast/brightness window setting 'left' of '{settings['left']}' must satisfy '0 <= left < {origin_limit.max}'. Origin is in top-left corner of the field of view.", 

1651 ), 

1652 "top": And( 

1653 float, 

1654 lambda x: ut.in_interval( 

1655 x, 

1656 limit=origin_limit, 

1657 type=tbt.IntervalType.RIGHT_OPEN, 

1658 ), 

1659 error=f"In step '{step_name}', requested auto contrast/brightness windows setting 'top' of '{settings['top']}' must satisfy '0.0 <= top < {origin_limit.max}'. Origin is in top-left corner of the field of view.", 

1660 ), 

1661 "width": And( 

1662 float, 

1663 lambda x: ut.in_interval( 

1664 x, 

1665 limit=width_limit, 

1666 type=tbt.IntervalType.LEFT_OPEN, 

1667 ), 

1668 error=f"In step '{step_name}', requested auto contrast/brightness windows setting 'width' of {settings['width']} must satisfy '0.0 < width <= {width_limit.max}' with 'left' setting of {settings['left']} as total width ('left' + 'width') cannot exceed 1.0", 

1669 ), 

1670 "height": And( 

1671 float, 

1672 lambda x: ut.in_interval( 

1673 x, 

1674 limit=height_limit, 

1675 type=tbt.IntervalType.LEFT_OPEN, 

1676 ), 

1677 error=f"In step '{step_name}', requested auto contrast/brightness windows setting 'height' of {settings['height']} must satisfy '0.0 < height <= {height_limit.max}' with 'top' setting of {settings['top']} as total height ('top' + 'height') cannot exceed 1.0", 

1678 ), 

1679 }, 

1680 ignore_extra_keys=True, 

1681 ) 

1682 

1683 try: 

1684 schema.validate(settings) 

1685 except UnboundLocalError: 

1686 raise ValueError( 

1687 f"Error. Unsupported yml version {yml_format.version} provided." 

1688 ) 

1689 return True 

1690 

1691 

1692def validate_stage_position( 

1693 microscope: tbt.Microscope, 

1694 step_name: str, 

1695 settings: dict, 

1696 yml_format: tbt.YMLFormatVersion, 

1697) -> bool: 

1698 """ 

1699 Perform schema checking for stage position dictionary. 

1700 

1701 This function validates the stage position settings dictionary based on the specified yml format and the stage limits of the microscope. 

1702 

1703 ## Parameters 

1704 

1705 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the stage position settings. 

1706 - `step_name` (`str`): The name of the step in the .yml file. 

1707 - `settings` (`dict`): The dictionary containing the stage position settings. 

1708 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1709 

1710 ## Returns 

1711 

1712 - `bool`: True if the settings are valid, False otherwise. 

1713 

1714 ## Raises 

1715 

1716 - `ValueError`: If the yml version is unsupported. 

1717 """ 

1718 limits = stage_limits(microscope=microscope) 

1719 

1720 if yml_format.version >= 1.0: 1720 ↛ 1772line 1720 didn't jump to line 1772 because the condition on line 1720 was always true

1721 schema = Schema( 1721 ↛ 1772line 1721 didn't jump to line 1772 because

1722 { 

1723 "x_mm": And( 

1724 float, 

1725 lambda x: ut.in_interval( 

1726 x, 

1727 limit=limits.x_mm, 

1728 type=tbt.IntervalType.CLOSED, 

1729 ), 

1730 error=f"Requested x-axis position of {settings['x_mm']} mm for step '{step_name}' must satisfy '{limits.x_mm.min} <= x_mm <= {limits.x_mm.max}'", 

1731 ), 

1732 "y_mm": And( 

1733 float, 

1734 lambda x: ut.in_interval( 

1735 x, 

1736 limit=limits.y_mm, 

1737 type=tbt.IntervalType.CLOSED, 

1738 ), 

1739 error=f"Requested y-axis position of {settings['y_mm']} mm for step '{step_name}' must satisfy '{limits.y_mm.min} <= y_mm <= {limits.y_mm.max}'", 

1740 ), 

1741 "z_mm": And( 

1742 float, 

1743 lambda x: ut.in_interval( 

1744 x, 

1745 limit=limits.z_mm, 

1746 type=tbt.IntervalType.CLOSED, 

1747 ), 

1748 error=f"Requested z-axis position of {settings['z_mm']} mm for step '{step_name}' must satisfy '{limits.z_mm.min} <= z_mm <= {limits.z_mm.max}'", 

1749 ), 

1750 "r_deg": And( 

1751 float, 

1752 lambda x: ut.in_interval( 

1753 x, 

1754 limit=limits.r_deg, 

1755 type=tbt.IntervalType.RIGHT_OPEN, 

1756 ), 

1757 error=f"Requested r-axis position of {settings['r_deg']} degree for step '{step_name}' must satisfy '{limits.r_deg.min} <= r_deg < {limits.r_deg.max}'", 

1758 ), 

1759 "t_deg": And( 

1760 float, 

1761 lambda x: ut.in_interval( 

1762 x, 

1763 limit=limits.t_deg, 

1764 type=tbt.IntervalType.CLOSED, 

1765 ), 

1766 error=f"Requested r-axis position of {settings['t_deg']} degree for step '{step_name}' must satisfy '{limits.t_deg.min} <= t_deg <= {limits.t_deg.max}'", 

1767 ), 

1768 }, 

1769 ignore_extra_keys=True, 

1770 ) 

1771 

1772 try: 

1773 schema.validate(settings) 

1774 except UnboundLocalError: 

1775 raise ValueError( 

1776 f"Error. Unsupported yml version {yml_format.version} provided." 

1777 ) 

1778 

1779 

1780def validate_beam_settings( 

1781 microscope: tbt.Microscope, 

1782 beam_type: tbt.BeamType, 

1783 settings: dict, 

1784 yml_format: tbt.YMLFormatVersion, 

1785 step_name: str, 

1786) -> bool: 

1787 """ 

1788 Perform schema checking for beam setting dictionary. 

1789 

1790 This function validates the beam settings dictionary based on the specified yml format and the beam limits of the microscope. 

1791 

1792 ## Parameters 

1793 

1794 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the beam settings. 

1795 - `beam_type` (`tbt.BeamType`): The type of the beam (electron or ion). 

1796 - `settings` (`dict`): The dictionary containing the beam settings. 

1797 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1798 - `step_name` (`str`): The name of the step in the .yml file. 

1799 

1800 ## Returns 

1801 

1802 - `bool`: True if the settings are valid, False otherwise. 

1803 

1804 ## Raises 

1805 

1806 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

1807 """ 

1808 specified_beam = beam_object_type(beam_type)(settings=tbt.BeamSettings()) 

1809 selected_beam = ut.beam_type(specified_beam, microscope) 

1810 

1811 limits = beam_limits(selected_beam, beam_type) 

1812 

1813 if yml_format.version >= 1.0: 1813 ↛ 1897line 1813 didn't jump to line 1897 because the condition on line 1813 was always true

1814 schema = Schema( 

1815 { 

1816 "voltage_kv": And( 

1817 float, 

1818 lambda x: ut.in_interval( 

1819 x, 

1820 limit=limits.voltage_kv, 

1821 type=tbt.IntervalType.CLOSED, 

1822 ), 

1823 error=f"In step '{step_name}', requested voltage of '{settings['voltage_kv']}' kV not within limits of {limits.voltage_kv.min} kV and {limits.voltage_kv.max} kV.", 

1824 ), 

1825 "voltage_tol_kv": And( 

1826 float, 

1827 lambda x: x > 0, 

1828 error=f"In step '{step_name}', requested voltage tolerance of '{settings['voltage_tol_kv']}' kV must be a positive float (greater than 0).", 

1829 ), 

1830 "current_na": And( 

1831 float, 

1832 lambda x: ut.in_interval( 

1833 x, 

1834 limit=limits.current_na, 

1835 type=tbt.IntervalType.CLOSED, 

1836 ), 

1837 error=f"In step '{step_name}', requested voltage of '{settings['current_na']}' nA not within limits of {limits.current_na.min} nA and {limits.current_na.max} nA", 

1838 ), 

1839 "current_tol_na": And( 

1840 float, 

1841 lambda x: x > 0, 

1842 error=f"In step '{step_name}', 'current_tol_na' must be a positive float (greater than 0)", 

1843 ), 

1844 "hfw_mm": And( 

1845 float, 

1846 lambda x: ut.in_interval( 

1847 x, 

1848 limits.hfw_mm, 

1849 type=tbt.IntervalType.CLOSED, 

1850 ), 

1851 error=f"In step '{step_name}', requested horizontal field width of '{settings['hfw_mm']}' mm not within limits of {limits.hfw_mm.min} mm and {limits.hfw_mm.max} mm", 

1852 ), 

1853 "working_dist_mm": And( 

1854 float, 

1855 lambda x: ut.in_interval( 

1856 x, 

1857 limit=limits.working_distance_mm, 

1858 type=tbt.IntervalType.CLOSED, 

1859 ), 

1860 error=f"In step '{step_name}', requested working distance of '{settings['working_dist_mm']}' mm not within limits of {limits.working_distance_mm.min} mm and {limits.working_distance_mm.max} mm", 

1861 ), 

1862 }, 

1863 ignore_extra_keys=True, 

1864 ) 

1865 

1866 e_beam_schema = Schema( 

1867 { 

1868 "dynamic_focus": Or( 

1869 None, 

1870 bool, 

1871 error=f"In step '{step_name}' with 'electron' beam imaging, 'dynamic_focus' must be a boolean value but '{settings['dynamic_focus']}' of type {type(settings['dynamic_focus'])} was requested.", 

1872 ), 

1873 "tilt_correction": Or( 

1874 None, 

1875 bool, 

1876 error=f"In step '{step_name}' with 'electron' beam imaging, 'tilt_correction' must be a boolean value but '{settings['tilt_correction']}' of type {type(settings['tilt_correction'])} was requested.", 

1877 ), 

1878 }, 

1879 ignore_extra_keys=True, 

1880 ) 

1881 i_beam_schema = Schema( 

1882 { 

1883 "dynamic_focus": Or( 

1884 None, 

1885 False, 

1886 error=f"In step '{step_name}' with 'ion' beam imaging, 'dynamic_focus' must be 'False' or 'None' but '{settings['dynamic_focus']}' of type {type(settings['dynamic_focus'])} was requested.", 

1887 ), 

1888 "tilt_correction": Or( 

1889 None, 

1890 False, 

1891 error=f"In step '{step_name}' with 'ion' beam imaging, 'tilt_correction' must be 'False' or 'None' but '{settings['tilt_correction']}' of type {type(settings['tilt_correction'])} was requested.", 

1892 ), 

1893 }, 

1894 ignore_extra_keys=True, 

1895 ) 

1896 

1897 try: 

1898 schema.validate(settings) 

1899 if specified_beam.type == tbt.BeamType.ELECTRON: 1899 ↛ 1901line 1899 didn't jump to line 1901 because the condition on line 1899 was always true

1900 e_beam_schema.validate(settings) 

1901 if specified_beam.type == tbt.BeamType.ION: 1901 ↛ exitline 1901 didn't return from function 'validate_beam_settings' because the condition on line 1901 was always true

1902 i_beam_schema.validate(settings) 

1903 except UnboundLocalError: 

1904 raise ValueError( 

1905 f"Error. Unsupported yml version {yml_format.version} provided." 

1906 ) 

1907 

1908 

1909def validate_detector_settings( 

1910 microscope: tbt.Microscope, 

1911 beam_type: tbt.BeamType, 

1912 settings: dict, 

1913 yml_format: tbt.YMLFormatVersion, 

1914 step_name: str, 

1915) -> bool: 

1916 """ 

1917 Perform schema checking for detector setting dictionary. 

1918 

1919 This function validates the detector settings dictionary based on the specified yml format and the detector capabilities of the microscope. 

1920 

1921 ## Parameters 

1922 

1923 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the detector settings. 

1924 - `beam_type` (`tbt.BeamType`): The type of the beam (electron or ion). 

1925 - `settings` (`dict`): The dictionary containing the detector settings. 

1926 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

1927 - `step_name` (`str`): The name of the step in the .yml file. 

1928 

1929 ## Returns 

1930 

1931 - `bool`: True if the settings are valid, False otherwise. 

1932 

1933 ## Raises 

1934 

1935 - `KeyError`: If auto contrast/brightness settings conflict with fixed brightness/contrast values. 

1936 - `ValueError`: If the yml version is unsupported, or if the settings do not satisfy the specified schema, or if the detector type or mode is unsupported. 

1937 """ 

1938 

1939 # switch to top left quad, enable e-beam 

1940 devices.device_access(microscope=microscope) 

1941 

1942 # set specified beam to active imaging device 

1943 specified_beam = beam_object_type(beam_type)(settings=tbt.BeamSettings()) 

1944 img.set_beam_device( 

1945 microscope=microscope, 

1946 device=specified_beam.device, 

1947 ) 

1948 

1949 auto_cb_db = settings.get("auto_cb") 

1950 use_auto_cb = not ut.none_value_dictionary(settings.get("auto_cb")) 

1951 if use_auto_cb and ( 1951 ↛ 1958line 1951 didn't jump to line 1958 because the condition on line 1951 was always true

1952 settings.get("brightness") is not None or settings.get("contrast") is not None 

1953 ): 

1954 raise KeyError( 

1955 f"Auto contrast/brightness settings of {auto_cb_db} provided while settings of fixed brightness of '{settings.get('brightness')}' and fixed contrast of '{settings.get('contrast')}' were also provided. Users may use either fixed brightness/contrast values or auto contrast/brightness settings, but not both." 

1956 ) 

1957 

1958 if yml_format.version >= 1.0: 1958 ↛ 1959,   1958 ↛ 19932 missed branches: 1) line 1958 didn't jump to line 1959 because the condition on line 1958 was never true, 2) line 1958 didn't jump to line 1993 because the condition on line 1958 was always true

1959 detector = settings.get("type") 

1960 mode = settings.get("mode") 

1961 cb_limit = tbt.Limit(min=0.0, max=1.0) 

1962 schema = Schema( 1962 ↛ 1993line 1962 didn't jump to line 1993 because

1963 { 

1964 "brightness": Or( 

1965 None, 

1966 And( 

1967 float, 

1968 lambda x: ut.in_interval( 

1969 x, 

1970 limit=cb_limit, 

1971 type=tbt.IntervalType.CLOSED, 

1972 ), 

1973 ), 

1974 error=f"In step '{step_name}', requested fixed brightness of '{settings['brightness']}'. This is not within limits of '>={cb_limit.min}' and '<={cb_limit.max}'.", 

1975 ), 

1976 "contrast": Or( 

1977 None, 

1978 And( 

1979 float, 

1980 lambda x: ut.in_interval( 

1981 x, 

1982 limit=cb_limit, 

1983 type=tbt.IntervalType.CLOSED, 

1984 ), 

1985 ), 

1986 error=f"In step '{step_name}', requested fixed contrast of '{settings['contrast']}'. This is not within limits of '>={cb_limit.min}' and '<={cb_limit.max}'.", 

1987 ), 

1988 }, 

1989 ignore_extra_keys=True, 

1990 ) 

1991 

1992 # check detector type 

1993 if not ut.valid_enum_entry(detector, tbt.DetectorType): 1993 ↛ 1994,   1993 ↛ 19972 missed branches: 1) line 1993 didn't jump to line 1994 because the condition on line 1993 was never true, 2) line 1993 didn't jump to line 1997 because the condition on line 1993 was always true

1994 raise ValueError( 

1995 f"Unsupported detector type of '{detector}' on step '{step_name}'." 

1996 ) 

1997 detector_type = tbt.DetectorType(detector) 

1998 microscope_detector_types = available_detector_types(microscope=microscope) 

1999 if detector_type.value not in microscope_detector_types: 

2000 raise ValueError( 

2001 f"Requested detector of {detector_type.value} is unavailable on this tool. Available detectors are: {microscope_detector_types}" 

2002 ) 

2003 # make sure to set this detector as the active one to access modes 

2004 img.detector_type( 

2005 microscope=microscope, 

2006 detector=detector_type, 

2007 ) 

2008 

2009 # check detector mode 

2010 if not ut.valid_enum_entry(mode, tbt.DetectorMode): 2010 ↛ 2011,   2010 ↛ 20142 missed branches: 1) line 2010 didn't jump to line 2011 because the condition on line 2010 was never true, 2) line 2010 didn't jump to line 2014 because the condition on line 2010 was always true

2011 raise ValueError( 

2012 f'Unsupported detector mode of "{mode}" for "{detector}" detector' 

2013 ) 

2014 detector_mode = tbt.DetectorMode(mode) 

2015 microscope_detector_modes = available_detector_modes(microscope=microscope) 

2016 if detector_mode.value not in microscope_detector_modes: 

2017 raise ValueError( 

2018 f"Requested mode of {detector_mode.value} for {detector_type.value} detector is invalid. Valid mode types are: {microscope_detector_modes}" 

2019 ) 

2020 

2021 # check detector settings 

2022 try: 

2023 schema.validate(settings) 

2024 except UnboundLocalError: 

2025 raise ValueError( 

2026 f"Error. Unsupported yml version {yml_format.version} provided." 

2027 ) 

2028 

2029 

2030def validate_EBSD_EDS_settings( 

2031 ebsd_oem: str, 

2032 eds_oem: str, 

2033) -> bool: 

2034 """ 

2035 Check EBSD and EDS OEM and connection for supported OEMs. 

2036 

2037 This function ensures that the specified EBSD and EDS OEMs are supported and that the connection settings are valid. 

2038 

2039 ## Parameters 

2040 

2041 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2042 - `connection_host` (`str`): The host for the microscope connection. 

2043 - `connection_port` (`str`): The port for the microscope connection. 

2044 - `ebsd_oem` (`str`): The OEM for the EBSD device. 

2045 - `eds_oem` (`str`): The OEM for the EDS device. 

2046 

2047 ## Returns 

2048 

2049 - `bool`: True if the settings are valid, False otherwise. 

2050 

2051 ## Raises 

2052 

2053 - `NotImplementedError`: If differing EBSD and EDS OEMs are requested. 

2054 - `ValueError`: If the EBSD or EDS OEM is unsupported. 

2055 - `SystemError`: If the Laser API is not accessible. 

2056 """ 

2057 # ensure same manufacturer for both EBSD and EDS 

2058 if (ebsd_oem is not None) and (eds_oem is not None) and (ebsd_oem != eds_oem): 

2059 raise NotImplementedError( 

2060 f"Differing EBSD and EDS OEMs are not supported. Requested EBSD OEM of '{ebsd_oem}' and EDS OEM of '{eds_oem}'." 

2061 ) 

2062 

2063 # Check EBSD OEM 

2064 if not ut.valid_enum_entry(ebsd_oem, tbt.ExternalDeviceOEM): 2064 ↛ 2068line 2064 didn't jump to line 2068 because the condition on line 2064 was always true

2065 raise ValueError( 2065 ↛ exit,   2065 ↛ exit2 missed branches: 1) line 2065 didn't jump to the function exit, 2) line 2065 didn't except from function 'validate_EBSD_EDS_settings' because the raise on line 2065 wasn't executed

2066 f"Unsupported EBSD OEM of '{ebsd_oem}'. Supported OEM types are: {[i.value for i in tbt.ExternalDeviceOEM]}" 

2067 ) 

2068 ebsd_device = tbt.ExternalDeviceOEM(ebsd_oem) 

2069 # Check EDS OEM 

2070 if not ut.valid_enum_entry(eds_oem, tbt.ExternalDeviceOEM): 

2071 raise ValueError( 

2072 f"Unsupported EDS OEM of '{eds_oem}'. Supported OEM types are: {[i.value for i in tbt.ExternalDeviceOEM]}" 

2073 ) 

2074 eds_device = tbt.ExternalDeviceOEM(eds_oem) 

2075 

2076 # exit if both devices are none, no need for laser control 

2077 if ebsd_device == eds_device == tbt.ExternalDeviceOEM.NONE: 

2078 return True 

2079 

2080 # check EBSD and EDS connection 

2081 try: 

2082 fs_laser.tfs_laser 

2083 except: 

2084 raise SystemError( 

2085 "EBSD and/or EDS control requested, but Laser API not accessible, so cannot use EBSD and EDS control. Please restart Laser API, or if not installed, change OEM to 'null', or leave blank in settings file." 

2086 ) 

2087 

2088 ###################### PROPOSED CHANGE ###################### 

2089 # Retracting devices here is unnecessary 

2090 # This causes devices to retract during validation 

2091 # Device retraction is needed only when startin/ending experiments 

2092 # Both of which are outside the scope of validation and already handled elsewhere 

2093 ###################### PROPOSED CHANGE ###################### 

2094 # microscope = tbt.Microscope() 

2095 # ut.connect_microscope( 

2096 # microscope=microscope, 

2097 # quiet_output=True, 

2098 # connection_host=connection_host, 

2099 # connection_port=connection_port, 

2100 # ) 

2101 # if tbt.ExternalDeviceOEM(eds_oem) != tbt.ExternalDeviceOEM.NONE: 

2102 # devices.retract_EDS(microscope=microscope) 

2103 # if tbt.ExternalDeviceOEM(ebsd_oem) != tbt.ExternalDeviceOEM.NONE: 

2104 # devices.retract_EBSD(microscope=microscope) 

2105 # ut.disconnect_microscope( 

2106 # microscope=microscope, 

2107 # quiet_output=True, 

2108 # ) 

2109 

2110 return True 

2111 

2112 

2113def validate_general_settings( 

2114 settings: dict, 

2115 yml_format: tbt.YMLFormatVersion, 

2116) -> bool: 

2117 """ 

2118 Perform schema checking for general setting dictionary. 

2119 

2120 This function validates the general settings dictionary based on the specified yml format. It checks the microscope connection and EBSD/EDS connection if valid OEMs are specified. 

2121 

2122 ## Parameters 

2123 

2124 - `settings` (`dict`): The dictionary containing the general settings. 

2125 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2126 

2127 ## Returns 

2128 

2129 - `bool`: True if the settings are valid, False otherwise. 

2130 

2131 ## Raises 

2132 

2133 - `ValueError`: If the general settings dictionary is empty, or if the settings do not satisfy the specified schema, or if the connection is invalid. 

2134 - `NotImplementedError`: If the sectioning axis is unsupported. 

2135 """ 

2136 

2137 if settings == {}: 2137 ↛ 2138,   2137 ↛ 21402 missed branches: 1) line 2137 didn't jump to line 2138 because the condition on line 2137 was never true, 2) line 2137 didn't jump to line 2140 because the condition on line 2137 was always true

2138 raise ValueError("General settings dictionary is empty.") 

2139 

2140 slice_thickness_limit_um = Constants.slice_thickness_limit_um 

2141 pre_tilt_limit_deg = Constants.pre_tilt_limit_deg_generic 

2142 

2143 if yml_format.version >= 1.0: 

2144 sectioning_axis = settings.get("sectioning_axis") 

2145 connection_host = settings.get("connection_host") 

2146 connection_port = settings.get("connection_port") 

2147 ebsd_oem = settings.get("EBSD_OEM") 

2148 eds_oem = settings.get("EDS_OEM") 

2149 exp_dir = settings.get("exp_dir") 

2150 h5_log_name = settings.get("h5_log_name") 

2151 

2152 # Validate the non-numeric values 

2153 # Check sectioning axis 

2154 if not ut.valid_enum_entry(sectioning_axis, tbt.SectioningAxis): 2154 ↛ 2155,   2154 ↛ 21572 missed branches: 1) line 2154 didn't jump to line 2155 because the condition on line 2154 was never true, 2) line 2154 didn't jump to line 2157 because the condition on line 2154 was always true

2155 raise ValueError(f"Unsupported sectioning axis of {sectioning_axis}.") 

2156 # TODO 

2157 if tbt.SectioningAxis(sectioning_axis) != tbt.SectioningAxis.Z: 

2158 raise NotImplementedError("Currently only Z-axis sectioning is supported.") 

2159 if tbt.SectioningAxis(sectioning_axis) != tbt.SectioningAxis.Z: 

2160 pre_tilt_limit_deg = Constants.pre_tilt_limit_deg_non_Z_sectioning # overwrite 

2161 warnings.warn( 

2162 "Pre-tilt value must be zero (0.0) degrees when using a sectioning axis other than 'Z'" 

2163 ) 

2164 # Check connection host and port 

2165 if not ut.valid_microscope_connection( 

2166 host=connection_host, 

2167 port=connection_port, 

2168 ): 

2169 raise ValueError( 

2170 f"Unsupported connection with host of {connection_host} and port of {connection_port}." 

2171 ) 

2172 

2173 # check EBSD and EDS 

2174 validate_EBSD_EDS_settings( 

2175 ebsd_oem=ebsd_oem, 

2176 eds_oem=eds_oem, 

2177 ) 

2178 

2179 # Check exp dir 

2180 try: 

2181 Path(exp_dir).mkdir( 

2182 parents=True, 

2183 exist_ok=True, 

2184 ) 

2185 except TypeError: 

2186 raise ValueError( 

2187 f'Requested experimental directory of "{exp_dir}", which is not a valid path.' 

2188 ) 

2189 # Check h5 log name 

2190 if not isinstance(h5_log_name, str): 

2191 raise ValueError(f'Unsupported h5 log name of "{h5_log_name}"') 

2192 

2193 schema = Schema( 2193 ↛ 2237line 2193 didn't jump to line 2237 because

2194 { 

2195 "slice_thickness_um": And( 

2196 float, 

2197 lambda x: ut.in_interval( 

2198 x, 

2199 limit=slice_thickness_limit_um, 

2200 type=tbt.IntervalType.CLOSED, 

2201 ), 

2202 error=f"Requested slice thickness of {settings['slice_thickness_um']} um must satisfy '{slice_thickness_limit_um.min} <= slice_thickness_um <= {slice_thickness_limit_um.max}'", 

2203 ), 

2204 "max_slice_num": And( 

2205 int, 

2206 lambda x: x > 0, 

2207 error=f"Requested max slice number of {settings['max_slice_num']} must satisfy '0 < max_slice_number'", 

2208 ), 

2209 "pre_tilt_deg": And( 

2210 float, 

2211 lambda x: ut.in_interval( 

2212 x, 

2213 limit=pre_tilt_limit_deg, 

2214 type=tbt.IntervalType.CLOSED, 

2215 ), 

2216 error=f"Requested pre tilt of {settings['pre_tilt_deg']} degrees must satisfy '{pre_tilt_limit_deg.min} <= pre_tilt_deg <= {pre_tilt_limit_deg.max}'", 

2217 ), 

2218 "stage_translational_tol_um": And( 

2219 float, 

2220 lambda x: x > 0, 

2221 error=f"Requested stage translational tolerance of {settings['stage_translational_tol_um']} um must be a positive float (greater than 0)", 

2222 ), 

2223 "stage_angular_tol_deg": And( 

2224 float, 

2225 lambda x: x > 0, 

2226 error=f"Requested stage angular tolerance of {settings['stage_angular_tol_deg']} degrees must be a positive float (greater than 0)", 

2227 ), 

2228 "step_count": And( 

2229 int, 

2230 lambda x: x > 0, 

2231 error=f"Requested step count of {settings['step_count']} must be a positive int (greater than 0)", 

2232 ), 

2233 }, 

2234 ignore_extra_keys=True, 

2235 ) 

2236 

2237 try: 

2238 schema.validate(settings) 

2239 except UnboundLocalError: 

2240 raise ValueError( 

2241 f"Error. Unsupported yml version {yml_format.version} provided." 

2242 ) 

2243 

2244 

2245def validate_scan_settings( 

2246 microscope: tbt.Microscope, 

2247 beam_type: tbt.BeamType, 

2248 settings: dict, 

2249 yml_format: tbt.YMLFormatVersion, 

2250 step_name: str, 

2251) -> bool: 

2252 """ 

2253 Perform schema checking for scan setting dictionary. 

2254 

2255 This function validates the scan settings dictionary based on the specified yml format and the scan limits of the microscope. 

2256 

2257 ## Parameters 

2258 

2259 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the scan settings. 

2260 - `beam_type` (`tbt.BeamType`): The type of the beam (electron or ion). 

2261 - `settings` (`dict`): The dictionary containing the scan settings. 

2262 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2263 - `step_name` (`str`): The name of the step in the .yml file. 

2264 

2265 ## Returns 

2266 

2267 - `bool`: True if the settings are valid, False otherwise. 

2268 

2269 ## Raises 

2270 

2271 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

2272 """ 

2273 specified_beam = beam_object_type(beam_type)(settings=tbt.BeamSettings()) 

2274 selected_beam = ut.beam_type(specified_beam, microscope) 

2275 

2276 """Schema checking for scan setting dictionary, format specified by yml_format""" 

2277 if yml_format.version >= 1.0: 2277 ↛ 2284line 2277 didn't jump to line 2284 because the condition on line 2277 was always true

2278 limits = scan_limits(selected_beam) 

2279 resolution = settings["resolution"] 

2280 rotation_limit = limits.rotation_deg 

2281 dwell_limit = limits.dwell_us 

2282 

2283 # check resolution 

2284 res = string_to_res(resolution) 

2285 

2286 schema = Schema( 2286 ↛ 2315line 2286 didn't jump to line 2315 because

2287 { 

2288 "resolution": And( 

2289 str, 

2290 lambda x: valid_string_resolution(x), 

2291 error=f"In '{step_name}', resolution provided was {settings['resolution']}, but must be an integer width and height in format '[width]x[height]' with each dimension satisfying '{Constants.scan_resolution_limit.min} <= [value] <= {Constants.scan_resolution_limit.max}", 

2292 ), 

2293 "rotation_deg": And( 

2294 float, 

2295 lambda x: ut.in_interval( 

2296 x, 

2297 limit=rotation_limit, 

2298 type=tbt.IntervalType.CLOSED, 

2299 ), 

2300 error=f"In '{step_name}', requested fixed rotation of '{settings['rotation_deg']}' degrees. This is not a float or not within limits of '>={rotation_limit.min}' and '<={rotation_limit.max}' degrees. Setting is of type {type(settings['rotation_deg'])}.", 

2301 ), 

2302 "dwell_time_us": And( 

2303 float, 

2304 lambda x: ut.in_interval( 

2305 x, 

2306 limit=dwell_limit, 

2307 type=tbt.IntervalType.CLOSED, 

2308 ), 

2309 error=f"In '{step_name}', requested fixed dwell_time of '{settings['dwell_time_us']}' microseconds. This is a float or not within limits of '>={dwell_limit.min}' and '<={dwell_limit.max}' microseconds. Settting is of type {type(settings['dwell_time_us'])}.", 

2310 ), 

2311 }, 

2312 ignore_extra_keys=True, 

2313 ) 

2314 

2315 try: 

2316 schema.validate(settings) 

2317 except UnboundLocalError: 

2318 raise ValueError( 

2319 f"Error. Unsupported yml version {yml_format.version} provided." 

2320 ) 

2321 

2322 

2323def stage_position_settings( 

2324 microscope: tbt.Microscope, 

2325 step_name: str, 

2326 general_settings: tbt.GeneralSettings, 

2327 step_stage_settings: dict, 

2328 yml_format: tbt.YMLFormatVersion, 

2329) -> tbt.StageSettings: 

2330 """ 

2331 Create a StagePositionUser object from settings, including validation. 

2332 

2333 This function creates a `StagePositionUser` object from the provided settings and performs validation. 

2334 

2335 ## Parameters 

2336 

2337 - `microscope` (`tbt.Microscope`): The microscope object for which to set the stage position. 

2338 - `step_name` (`str`): The name of the step in the .yml file. 

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

2340 - `step_stage_settings` (`dict`): The dictionary containing the stage position settings for the step. 

2341 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2342 

2343 ## Returns 

2344 

2345 - `tbt.StageSettings`: The stage settings object. 

2346 

2347 ## Raises 

2348 

2349 - `NotImplementedError`: If the rotation side value is unsupported. 

2350 - `ValueError`: If the stage position settings do not satisfy the specified schema. 

2351 """ 

2352 

2353 if yml_format.version >= 1.0: 2353 ↛ 2354,   2353 ↛ 23622 missed branches: 1) line 2353 didn't jump to line 2354 because the condition on line 2353 was never true, 2) line 2353 didn't jump to line 2362 because the condition on line 2353 was always true

2354 pos_db = step_stage_settings.get("initial_position") 

2355 

2356 rotation_side = step_stage_settings["rotation_side"] 

2357 if not ut.valid_enum_entry(rotation_side, tbt.RotationSide): 

2358 raise NotImplementedError( 

2359 f"Unsupported rotation_side value of '{rotation_side}' of type '{type(rotation_side)}' in step '{step_name}', supported rotation_side values are: {[i.value for i in tbt.RotationSide]}." 

2360 ) 

2361 

2362 validate_stage_position( 

2363 microscope=microscope, 

2364 step_name=step_name, 

2365 settings=pos_db, 

2366 yml_format=yml_format, 

2367 ) 

2368 

2369 initial_position = tbt.StagePositionUser( 

2370 x_mm=pos_db["x_mm"], 

2371 y_mm=pos_db["y_mm"], 

2372 z_mm=pos_db["z_mm"], 

2373 r_deg=pos_db["r_deg"], 

2374 t_deg=pos_db["t_deg"], 

2375 ) 

2376 

2377 pretilt_angle_deg = general_settings.pre_tilt_deg 

2378 sectioning_axis = general_settings.sectioning_axis 

2379 

2380 stage_settings = tbt.StageSettings( 

2381 microscope=microscope, 

2382 initial_position=initial_position, 

2383 pretilt_angle_deg=pretilt_angle_deg, 

2384 sectioning_axis=sectioning_axis, 

2385 rotation_side=tbt.RotationSide(rotation_side), 

2386 # movement_mode=tbt.StageMovementMode.OUT_OF_PLANE, 

2387 ) 

2388 

2389 return stage_settings 

2390 

2391 

2392def validate_pulse_settings( 

2393 settings: dict, 

2394 yml_format: tbt.YMLFormatVersion, 

2395 step_name: str, 

2396) -> bool: 

2397 """ 

2398 Perform schema checking for pulse setting dictionary. 

2399 

2400 This function validates the pulse settings dictionary based on the specified yml format. 

2401 

2402 ## Parameters 

2403 

2404 - `settings` (`dict`): The dictionary containing the pulse settings. 

2405 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2406 - `step_name` (`str`): The name of the step in the .yml file. 

2407 

2408 ## Returns 

2409 

2410 - `bool`: True if the settings are valid, False otherwise. 

2411 

2412 ## Raises 

2413 

2414 - `NotImplementedError`: If the wavelength or polarization value is unsupported. 

2415 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

2416 """ 

2417 if yml_format.version >= 1.0: 2417 ↛ 2418,   2417 ↛ 24442 missed branches: 1) line 2417 didn't jump to line 2418 because the condition on line 2417 was never true, 2) line 2417 didn't jump to line 2444 because the condition on line 2417 was always true

2418 wavelength_nm = settings.get("wavelength_nm") 

2419 if not ut.valid_enum_entry(wavelength_nm, tbt.LaserWavelength): 

2420 raise NotImplementedError( 

2421 f"In 'laser' step_type for step '{step_name}', unsupported wavelength of '{wavelength_nm}' nm (data type {type(wavelength_nm)}), supported wavelengths are: {[i.value for i in tbt.LaserWavelength]}." 

2422 ) 

2423 polarization = settings.get("polarization") 

2424 if not ut.valid_enum_entry(polarization, tbt.LaserPolarization): 

2425 raise NotImplementedError( 

2426 f"In 'laser' step_type for step '{step_name}', unsupported laser polarization of '{polarization}', supported values are: {[i.value for i in tbt.LaserPolarization]}." 

2427 ) 

2428 

2429 schema = Schema( 2429 ↛ exit,   2429 ↛ 24442 missed branches: 1) line 2429 didn't jump to the function exit, 2) line 2429 didn't jump to line 2444 because

2430 { 

2431 "divider": And( 

2432 int, 

2433 lambda x: x > 0, 

2434 error=f"In 'laser' step_type for step '{step_name}', 'divider' parameter must be a positive integer greater than 0 but '{settings['divider']}' was requested.", 

2435 ), 

2436 "energy_uj": And( 

2437 float, 

2438 lambda x: x > 0, 

2439 error=f"In 'laser' step_type for step '{step_name}', 'energy_uj' parameter must be a positive float greater than 0 but '{settings['energy_uj']}' was requested.", 

2440 ), 

2441 }, 

2442 ignore_extra_keys=True, 

2443 ) 

2444 try: 

2445 schema.validate(settings) 

2446 except UnboundLocalError: 

2447 raise ValueError( 

2448 f"Error. Unsupported yml version {yml_format.version} provided." 

2449 ) 

2450 

2451 

2452def validate_laser_optics_settings( 

2453 settings: dict, 

2454 yml_format: tbt.YMLFormatVersion, 

2455 step_name: str, 

2456) -> bool: 

2457 """ 

2458 Perform schema checking for laser optics setting dictionary. 

2459 

2460 This function validates the laser optics settings dictionary based on the specified yml format. 

2461 

2462 ## Parameters 

2463 

2464 - `settings` (`dict`): The dictionary containing the laser optics settings. 

2465 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2466 - `step_name` (`str`): The name of the step in the .yml file. 

2467 

2468 ## Returns 

2469 

2470 - `bool`: True if the settings are valid, False otherwise. 

2471 

2472 ## Raises 

2473 

2474 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

2475 """ 

2476 if yml_format.version >= 1.0: 2476 ↛ 2499line 2476 didn't jump to line 2499 because the condition on line 2476 was always true

2477 schema = Schema( 2477 ↛ exit,   2477 ↛ 24992 missed branches: 1) line 2477 didn't jump to the function exit, 2) line 2477 didn't jump to line 2499 because

2478 { 

2479 "objective_position_mm": And( 

2480 float, 

2481 lambda x: ut.in_interval( 

2482 x, 

2483 limit=Constants.laser_objective_limit_mm, 

2484 type=tbt.IntervalType.CLOSED, 

2485 ), 

2486 error=f"In 'laser' step_type for step '{step_name}', 'objective_position_mm' parameter must be a float satisfying the following: {Constants.laser_objective_limit_mm.min} mm <= value <= {Constants.laser_objective_limit_mm.max} mm. '{settings['objective_position_mm']}' mm (of type {type(settings['objective_position_mm'])}) was requested.", 

2487 ), 

2488 "beam_shift_um_x": And( 

2489 float, 

2490 error=f"In 'laser' step_type for step '{step_name}', 'x' parameter for 'beam_shift_um' sub-dictioanry must be a float but '{settings['beam_shift_um_x']}' (of type {type(settings['beam_shift_um_x'])}) was requested.", 

2491 ), 

2492 "beam_shift_um_y": And( 

2493 float, 

2494 error=f"In 'laser' step_type for step '{step_name}', 'y' parameter for 'beam_shift_um' sub-dictioanry must be a float but '{settings['beam_shift_um_y']}' (of type {type(settings['beam_shift_um_y'])}) was requested.", 

2495 ), 

2496 }, 

2497 ignore_extra_keys=True, 

2498 ) 

2499 try: 

2500 schema.validate(settings) 

2501 except UnboundLocalError: 

2502 raise ValueError( 

2503 f"Error. Unsupported yml version {yml_format.version} provided." 

2504 ) 

2505 

2506 

2507def validate_laser_box_settings( 

2508 settings: dict, 

2509 yml_format: tbt.YMLFormatVersion, 

2510 step_name: str, 

2511) -> bool: 

2512 """ 

2513 Perform schema checking for laser box pattern setting dictionary. 

2514 

2515 This function validates the laser box pattern settings dictionary based on the specified yml format. 

2516 

2517 ## Parameters 

2518 

2519 - `settings` (`dict`): The dictionary containing the laser box pattern settings. 

2520 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2521 - `step_name` (`str`): The name of the step in the .yml file. 

2522 

2523 ## Returns 

2524 

2525 - `bool`: True if the settings are valid, False otherwise. 

2526 

2527 ## Raises 

2528 

2529 - `NotImplementedError`: If the scan type or coordinate reference value is unsupported. 

2530 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

2531 """ 

2532 if yml_format.version >= 1.0: 

2533 # scan type 

2534 scan_type = settings.get("scan_type") 

2535 scan_types = [ 

2536 tbt.LaserScanType.RASTER, 

2537 tbt.LaserScanType.SERPENTINE, 

2538 ] 

2539 scan_type_error_msg = f"In 'laser' step_type for step '{step_name}', unsupported scan type of '{scan_type}' for box pattern, supported scan types are: {[i.value for i in scan_types]}." 

2540 if not ut.valid_enum_entry(scan_type, tbt.LaserScanType): 

2541 raise NotImplementedError(scan_type_error_msg) 

2542 if tbt.LaserScanType(scan_type) not in scan_types: 

2543 raise NotImplementedError(scan_type_error_msg) 

2544 

2545 # coordinate reference 

2546 coordinate_ref = settings.get("coordinate_ref") 

2547 coord_refs = [ 

2548 tbt.CoordinateReference.CENTER, 

2549 tbt.CoordinateReference.UPPER_CENTER, 

2550 tbt.CoordinateReference.UPPER_LEFT, 

2551 ] 

2552 coord_error_msg = f"In 'laser' step_type for step '{step_name}', unsupported coordinate reference of '{coordinate_ref}' for box pattern, supported coordinate references are: {[i.value for i in coord_refs]}." 2552 ↛ exitline 2552 didn't run the list comprehension on line 2552

2553 if not ut.valid_enum_entry(coordinate_ref, tbt.CoordinateReference): 

2554 raise NotImplementedError(coord_error_msg) 

2555 if tbt.CoordinateReference(coordinate_ref) not in coord_refs: 

2556 raise NotImplementedError(coord_error_msg) 

2557 

2558 schema = Schema( 2558 ↛ 2588line 2558 didn't jump to line 2588 because

2559 { 

2560 "passes": And( 

2561 int, 

2562 lambda x: x > 0, 

2563 error=f"In 'laser' step_type for step '{step_name}', 'passes' parameter in 'box' type pattern must be a positive integer. '{settings['passes']}' (of type {type(settings['passes'])}) was requested.", 

2564 ), 

2565 "size_x_um": And( 

2566 float, 

2567 lambda x: x > 0, 

2568 error=f"In 'laser' step_type for step '{step_name}', 'size_x_um' parameter in 'box' type pattern must be a positive float. '{settings['size_x_um']}' mm (of type {type(settings['size_x_um'])}) was requested.", 

2569 ), 

2570 "size_y_um": And( 

2571 float, 

2572 lambda x: x > 0, 

2573 error=f"In 'laser' step_type for step '{step_name}', 'size_y_um' parameter in 'box' type pattern must be a positive float. '{settings['size_y_um']}' mm (of type {type(settings['size_y_um'])}) was requested.", 

2574 ), 

2575 "pitch_x_um": And( 

2576 float, 

2577 lambda x: x > 0, 

2578 error=f"In 'laser' step_type for step '{step_name}', 'pitch_x_um' parameter in 'box' type pattern must be a positive float. '{settings['pitch_x_um']}' mm (of type {type(settings['pitch_x_um'])}) was requested.", 

2579 ), 

2580 "pitch_y_um": And( 

2581 float, 

2582 lambda x: x > 0, 

2583 error=f"In 'laser' step_type for step '{step_name}', 'pitch_y_um' parameter in 'box' type pattern must be a positive float. '{settings['pitch_y_um']}' mm (of type {type(settings['pitch_y_um'])}) was requested.", 

2584 ), 

2585 }, 

2586 ignore_extra_keys=True, 

2587 ) 

2588 try: 

2589 schema.validate(settings) 

2590 except UnboundLocalError: 

2591 raise ValueError( 

2592 f"Error. Unsupported yml version {yml_format.version} provided." 

2593 ) 

2594 

2595 

2596def validate_laser_line_settings( 

2597 settings: dict, 

2598 yml_format: tbt.YMLFormatVersion, 

2599 step_name: str, 

2600) -> bool: 

2601 """ 

2602 Perform schema checking for laser line pattern setting dictionary. 

2603 

2604 This function validates the laser line pattern settings dictionary based on the specified yml format. 

2605 

2606 ## Parameters 

2607 

2608 - `settings` (`dict`): The dictionary containing the laser line pattern settings. 

2609 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2610 - `step_name` (`str`): The name of the step in the .yml file. 

2611 

2612 ## Returns 

2613 

2614 - `bool`: True if the settings are valid, False otherwise. 

2615 

2616 ## Raises 

2617 

2618 - `NotImplementedError`: If the scan type value is unsupported. 

2619 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

2620 """ 

2621 if yml_format.version >= 1.0: 

2622 # scan type 

2623 scan_type = settings.get("scan_type") 

2624 scan_types = [ 

2625 tbt.LaserScanType.SINGLE, 

2626 tbt.LaserScanType.LAP, 

2627 ] 

2628 scan_type_error_msg = f"In 'laser' step_type for step '{step_name}', unsupported scan type of '{scan_type}' for line pattern, supported scan types are: {[i.value for i in scan_types]}." 

2629 if not ut.valid_enum_entry(scan_type, tbt.LaserScanType): 

2630 raise NotImplementedError(scan_type_error_msg) 

2631 if tbt.LaserScanType(scan_type) not in scan_types: 

2632 raise NotImplementedError(scan_type_error_msg) 

2633 

2634 schema = Schema( 2634 ↛ exit,   2634 ↛ 26542 missed branches: 1) line 2634 didn't jump to the function exit, 2) line 2634 didn't jump to line 2654 because

2635 { 

2636 "passes": And( 

2637 int, 

2638 lambda x: x > 0, 

2639 error=f"In 'laser' step_type for step '{step_name}', 'passes' parameter in 'line' type pattern must be a positive integer. '{settings['passes']}' (of type {type(settings['passes'])}) was requested.", 

2640 ), 

2641 "size_um": And( 

2642 float, 

2643 lambda x: x > 0, 

2644 error=f"In 'laser' step_type for step '{step_name}', 'size_um' parameter in 'line' type pattern must be a positive float. '{settings['size_um']}' mm (of type {type(settings['size_um'])}) was requested.", 

2645 ), 

2646 "pitch_um": And( 

2647 float, 

2648 lambda x: x > 0, 

2649 error=f"In 'laser' step_type for step '{step_name}', 'pitch_um' parameter in 'line' type pattern must be a positive float. '{settings['pitch_um']}' mm (of type {type(settings['pitch_um'])}) was requested.", 

2650 ), 

2651 }, 

2652 ignore_extra_keys=True, 

2653 ) 

2654 try: 

2655 schema.validate(settings) 

2656 except UnboundLocalError: 

2657 raise ValueError( 

2658 f"Error. Unsupported yml version {yml_format.version} provided." 

2659 ) 

2660 

2661 

2662def validate_laser_mode_settings( 

2663 settings: dict, 

2664 yml_format: tbt.YMLFormatVersion, 

2665 step_name: str, 

2666) -> bool: 

2667 """ 

2668 Perform schema checking for laser mode setting dictionary. 

2669 

2670 This function validates the laser mode settings dictionary based on the specified yml format. 

2671 

2672 ## Parameters 

2673 

2674 - `settings` (`dict`): The dictionary containing the laser mode settings. 

2675 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2676 - `step_name` (`str`): The name of the step in the .yml file. 

2677 

2678 ## Returns 

2679 

2680 - `bool`: True if the settings are valid, False otherwise. 

2681 

2682 ## Raises 

2683 

2684 - `NotImplementedError`: If the laser pattern mode value is unsupported. 

2685 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

2686 """ 

2687 if yml_format.version >= 1.0: 

2688 mode = settings.get("mode") 

2689 if not ut.valid_enum_entry(mode, tbt.LaserPatternMode): 

2690 raise NotImplementedError( 

2691 f"In 'laser' step_type for step '{step_name}', unsupported laser pattern mode of '{mode}', supported values are: {[i.value for i in tbt.LaserPatternMode]}." 

2692 ) 

2693 

2694 schema = Schema( 

2695 { 

2696 "rotation_deg": And( 

2697 float, 

2698 error=f"In 'laser' step_type for step '{step_name}', 'rotation_deg' parameter must be a float. '{settings['rotation_deg']}' degrees (of type {type(settings['rotation_deg'])}) was requested.", 

2699 ), 

2700 }, 

2701 ignore_extra_keys=True, 

2702 ) 

2703 schema_fine = Schema( 2703 ↛ exitline 2703 didn't jump to the function exit

2704 { 

2705 "pixel_dwell_ms": Or( 

2706 None, 

2707 "null", 

2708 "None", 

2709 error=f"In 'laser' step_type for step '{step_name}', 'pixel_dwell_ms' parameter does not apply to the selected 'fine' milling mode. Set 'pixel_dwell_ms' to 'null' 'None' or leave the entry blank to continue.", 

2710 ), 

2711 "pulses_per_pixel": And( 

2712 int, 

2713 lambda x: x > 0, 

2714 error=f"In 'laser' step_type for step '{step_name}', 'pulses_per_pixel' parameter is required for pattern type of 'fine'. 'pulses_per_pixel' must be a positive integer greater than 0. '{settings['pulses_per_pixel']}' (of type {type(settings['pulses_per_pixel'])}) was requested.", 

2715 ), 

2716 }, 

2717 ignore_extra_keys=True, 

2718 ) 

2719 schema_coarse = Schema( 2719 ↛ exit,   2719 ↛ 27352 missed branches: 1) line 2719 didn't jump to the function exit, 2) line 2719 didn't jump to line 2735 because

2720 { 

2721 "pixel_dwell_ms": And( 

2722 float, 

2723 lambda x: x > 0.0, 

2724 error=f"In 'laser' step_type for step '{step_name}', 'pixel_dwell_ms' parameter is required for pattern type of 'coarse'. 'pixel_dwell_ms' must be a positive float greater than 0. '{settings['pixel_dwell_ms']}' (of type {type(settings['pixel_dwell_ms'])}) was requested.", 

2725 ), 

2726 "pulses_per_pixel": Or( 

2727 None, 

2728 "null", 

2729 "None", 

2730 error=f"In 'laser' step_type for step '{step_name}', 'pulses_per_pixel' parameter does not apply to the selected 'coarse' milling mode. Set 'pulses_per_pixel' to 'null' 'None' or leave the entry blank to continue.", 

2731 ), 

2732 }, 

2733 ignore_extra_keys=True, 

2734 ) 

2735 try: 

2736 schema.validate(settings) 

2737 if mode == "fine": 2737 ↛ 2739line 2737 didn't jump to line 2739 because the condition on line 2737 was always true

2738 schema_fine.validate(settings) 

2739 if mode == "coarse": 

2740 schema_coarse.validate(settings) 

2741 except UnboundLocalError: 

2742 raise ValueError( 

2743 f"Error. Unsupported yml version {yml_format.version} provided." 

2744 ) 

2745 

2746 

2747def validate_laser_pattern_settings( 

2748 settings: dict, 

2749 yml_format: tbt.YMLFormatVersion, 

2750 step_name: str, 

2751) -> tbt.LaserPatternType: 

2752 """ 

2753 Perform schema checking for laser pattern setting dictionary. 

2754 

2755 This function validates the laser pattern settings dictionary based on the specified yml format and determines the type of pattern (box or line). 

2756 

2757 ## Parameters 

2758 

2759 - `settings` (`dict`): The dictionary containing the laser pattern settings. 

2760 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2761 - `step_name` (`str`): The name of the step in the .yml file. 

2762 

2763 ## Returns 

2764 

2765 - `tbt.LaserPatternType`: The type of the laser pattern (box or line). 

2766 

2767 ## Raises 

2768 

2769 - `KeyError`: If required settings are missing from the .yml file or if multiple pattern types are specified. 

2770 - `ValueError`: If the laser pattern settings are invalid. 

2771 """ 

2772 if yml_format.version >= 1.0: 2772 ↛ exit,   2772 ↛ 27742 missed branches: 1) line 2772 didn't return from function 'validate_laser_pattern_settings' because the condition on line 2772 was always true, 2) line 2772 didn't jump to line 2774 because the condition on line 2772 was never true

2773 # determine type of pattern (only one allowed) 

2774 type_set_db = settings.get("type") 

2775 if type_set_db is None: 

2776 raise KeyError( 

2777 f"Invalid .yml file, no 'type' settings sub-dictionary found in 'pattern' settings in 'laser' step_type for step '{step_name}'." 

2778 ) 

2779 

2780 box_settings = type_set_db.get("box") 

2781 line_settings = type_set_db.get("line") 

2782 if box_settings is None: 

2783 box_pattern = False 

2784 else: 

2785 box_pattern = not ut.none_value_dictionary(box_settings) 

2786 if line_settings is None: 2786 ↛ 2789line 2786 didn't jump to line 2789 because the condition on line 2786 was always true

2787 line_pattern = False 

2788 else: 

2789 line_pattern = not ut.none_value_dictionary(line_settings) 

2790 if box_pattern == line_pattern == False: 

2791 raise KeyError( 

2792 f"Invalid .yml file in 'laser' step_type for step '{step_name}'. No pattern type settings found." 

2793 ) 

2794 if box_pattern == line_pattern == True: 2794 ↛ 2798line 2794 didn't jump to line 2798 because the condition on line 2794 was always true

2795 raise KeyError( 

2796 f"Invalid .yml file in 'laser' step_type for step '{step_name}'. Pattern settings for one and only one type are allowed. Type settings found for both 'box' and 'line' type. Please leave one set of type settings completely blank, enter 'null' for each parameter, or remove the unused subdictionary completely from the .yml file." 

2797 ) 

2798 validate_laser_mode_settings( 

2799 settings=settings, 

2800 yml_format=yml_format, 

2801 step_name=step_name, 

2802 ) 

2803 

2804 if box_pattern: 

2805 validate_laser_box_settings( 

2806 settings=box_settings, 

2807 yml_format=yml_format, 

2808 step_name=step_name, 

2809 ) 

2810 return tbt.LaserPatternType.BOX 

2811 elif line_pattern: 2811 ↛ 2812,   2811 ↛ 28192 missed branches: 1) line 2811 didn't jump to line 2812 because the condition on line 2811 was never true, 2) line 2811 didn't jump to line 2819 because the condition on line 2811 was always true

2812 validate_laser_line_settings( 

2813 settings=line_settings, 

2814 yml_format=yml_format, 

2815 step_name=step_name, 

2816 ) 

2817 return tbt.LaserPatternType.LINE 

2818 else: 

2819 raise ValueError( 2819 ↛ exit,   2819 ↛ exit2 missed branches: 1) line 2819 didn't jump to the function exit, 2) line 2819 didn't except from function 'validate_laser_pattern_settings' because the raise on line 2819 wasn't executed

2820 f"Invalid laser pattern settings for step {step_name}. Supported types are {[i.value for i in tbt.LaserPatternType]}" 

2821 ) 

2822 

2823 

2824def validate_fib_pattern_settings( 

2825 microscope: tbt.Microscope, 

2826 settings: dict, 

2827 yml_format: tbt.YMLFormatVersion, 

2828 step_name: str, 

2829) -> Union[ 

2830 tbt.FIBRectanglePattern, 

2831 tbt.FIBRegularCrossSection, 

2832 tbt.FIBCleaningCrossSection, 

2833 tbt.FIBStreamPattern, 

2834]: 

2835 """ 

2836 Perform schema checking for FIB pattern setting dictionary. 

2837 

2838 This function validates the FIB pattern settings dictionary based on the specified yml format and determines the type of pattern (rectangle, regular cross section, cleaning cross section, or selected area). 

2839 

2840 ## Parameters 

2841 

2842 - `microscope` (`tbt.Microscope`): The microscope object for which to validate the FIB pattern settings. 

2843 - `settings` (`dict`): The dictionary containing the FIB pattern settings. 

2844 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

2845 - `step_name` (`str`): The name of the step in the .yml file. 

2846 

2847 ## Returns 

2848 

2849 - `Union[tbt.FIBRectanglePattern, tbt.FIBRegularCrossSection, tbt.FIBCleaningCrossSection, tbt.FIBStreamPattern]`: The validated FIB pattern object. 

2850 

2851 ## Raises 

2852 

2853 - `KeyError`: If required settings are missing from the .yml file or if multiple pattern types are specified. 

2854 - `ValueError`: If the application file is unsupported or invalid for the specified pattern type. 

2855 """ 

2856 

2857 if yml_format.version >= 1.0: 2857 ↛ 2858,   2857 ↛ 29172 missed branches: 1) line 2857 didn't jump to line 2858 because the condition on line 2857 was never true, 2) line 2857 didn't jump to line 2917 because the condition on line 2857 was always true

2858 application_file = settings.get("application_file") 

2859 # determine type of pattern (only one allowed) 

2860 type_set_db = settings.get("type") 

2861 if type_set_db is None: 2861 ↛ 2862,   2861 ↛ 28652 missed branches: 1) line 2861 didn't jump to line 2862 because the condition on line 2861 was never true, 2) line 2861 didn't jump to line 2865 because the condition on line 2861 was always true

2862 raise KeyError( 

2863 f"Invalid .yml file, no 'type' settings sub-dictionary found in 'pattern' settings in 'fib' step_type for step '{step_name}'." 

2864 ) 

2865 rectangle_settings = type_set_db.get("rectangle") 

2866 regular_cross_section_settings = type_set_db.get("regular_cross_section") 

2867 cleaning_cross_section_settings = type_set_db.get("cleaning_cross_section") 

2868 selected_area_settings = type_set_db.get("selected_area") 

2869 

2870 pattern_settings = [ 

2871 rectangle_settings, 

2872 regular_cross_section_settings, 

2873 cleaning_cross_section_settings, 

2874 selected_area_settings, 

2875 ] 

2876 ( 

2877 rectangle_pattern, 

2878 regular_cross_section_pattern, 

2879 cleaning_cross_section_pattern, 

2880 selected_area_pattern, 

2881 ) = (None, None, None, None) 

2882 # pattern_names = [val.value for val in tbt.FIBPatternType] # remembers order 

2883 # assert pattern_names == [ 

2884 # "rectangle", 

2885 # "regular_cross_section", 

2886 # "cleaning_cross_section", 

2887 # "selected_area", 

2888 # ] 

2889 pattern_names = [ 

2890 "rectangle", 

2891 "regular_cross_section", 

2892 "cleaning_cross_section", 

2893 "selected_area", 

2894 ] 

2895 pattern_types = [ 

2896 rectangle_pattern, 

2897 regular_cross_section_pattern, 

2898 cleaning_cross_section_pattern, 

2899 selected_area_pattern, 

2900 ] 

2901 

2902 for type in range(len(pattern_settings)): 

2903 db = pattern_settings[type] 

2904 if db is None: 2904 ↛ 2905,   2904 ↛ 29072 missed branches: 1) line 2904 didn't jump to line 2905 because the condition on line 2904 was never true, 2) line 2904 didn't jump to line 2907 because the condition on line 2904 was always true

2905 pattern_types[type] = False 

2906 else: 

2907 pattern_types[type] = not ut.none_value_dictionary(db) 

2908 

2909 # one and only one type of pattern can have settings 

2910 if sum(pattern_types) != 1: 

2911 raise KeyError( 2911 ↛ exit,   2911 ↛ exit2 missed branches: 1) line 2911 didn't jump to the function exit, 2) line 2911 didn't except from function 'validate_fib_pattern_settings' because the raise on line 2911 wasn't executed

2912 f"Invalid .yml file in 'fib' step_type for step '{step_name}'. Pattern settings for one and only one type are allowed. Please provide settings for only one of the supported pattern types: {[name for name in pattern_names]}. For unused pattern types, leave the type settings completely blank, enter 'null' for each parameter, or remove the unused subdictionary completely from the .yml file." 

2913 ) 

2914 pattern_type = tbt.FIBPatternType(pattern_names[pattern_types.index(True)]) 

2915 

2916 # check application file 

2917 valid_applications = application_files(microscope=microscope) 

2918 # TODO make this print out pretty 

2919 # display_applications = ",".join(valid_applications) 

2920 # display_applications = ut.tabular_list(data=valid_applications) 

2921 if application_file not in valid_applications: 2921 ↛ 2926line 2921 didn't jump to line 2926 because the condition on line 2921 was always true

2922 raise ValueError( 

2923 f"Unsupported FIB application file of '{application_file}' on step '{step_name}. Supported applications on this microscope are: \n{valid_applications}'" 

2924 ) 

2925 

2926 if pattern_type == tbt.FIBPatternType.RECTANGLE: 2926 ↛ 2969line 2926 didn't jump to line 2969 because the condition on line 2926 was always true

2927 validate_fib_box_settings( 

2928 settings=rectangle_settings, 

2929 yml_format=yml_format, 

2930 step_name=step_name, 

2931 pattern_type=pattern_type, 

2932 ) 

2933 

2934 pattern = tbt.FIBPattern( 

2935 application=application_file, 

2936 type=pattern_type, 

2937 geometry=tbt.FIBRectanglePattern( 

2938 center_um=tbt.Point( 

2939 x=rectangle_settings.get("center").get("x_um"), 

2940 y=rectangle_settings.get("center").get("y_um"), 

2941 ), 

2942 width_um=rectangle_settings.get("width_um"), 

2943 height_um=rectangle_settings.get("height_um"), 

2944 depth_um=rectangle_settings.get("depth_um"), 

2945 scan_direction=tbt.FIBPatternScanDirection( 

2946 rectangle_settings.get("scan_direction") 

2947 ), 

2948 scan_type=tbt.FIBPatternScanType(rectangle_settings.get("scan_type")), 

2949 ), 

2950 ) 

2951 # make sure application file is valid for this pattern type: 

2952 try: 

2953 geometry = pattern.geometry 

2954 microscope.patterning.set_default_application_file(pattern.application) 

2955 microscope.patterning.create_rectangle( 

2956 center_x=geometry.center_um.x * Conversions.UM_TO_M, 

2957 center_y=geometry.center_um.y * Conversions.UM_TO_M, 

2958 width=geometry.width_um * Conversions.UM_TO_M, 

2959 height=geometry.height_um * Conversions.UM_TO_M, 

2960 depth=geometry.depth_um * Conversions.UM_TO_M, 

2961 ) 

2962 microscope.patterning.clear_patterns() 

2963 except: 

2964 raise ValueError( 

2965 f'Invalid application file of "{pattern.application}" for Rectangle pattern type. Please select or create an appropriate application file.' 

2966 ) 

2967 

2968 return pattern 

2969 elif pattern_type == tbt.FIBPatternType.REGULAR_CROSS_SECTION: 

2970 validate_fib_box_settings( 

2971 settings=regular_cross_section_settings, 

2972 yml_format=yml_format, 

2973 step_name=step_name, 

2974 pattern_type=pattern_type, 

2975 ) 

2976 pattern = tbt.FIBPattern( 

2977 application=application_file, 

2978 type=pattern_type, 

2979 geometry=tbt.FIBRegularCrossSection( 

2980 center_um=tbt.Point( 

2981 x=regular_cross_section_settings.get("center").get("x_um"), 

2982 y=regular_cross_section_settings.get("center").get("y_um"), 

2983 ), 

2984 width_um=regular_cross_section_settings.get("width_um"), 

2985 height_um=regular_cross_section_settings.get("height_um"), 

2986 depth_um=regular_cross_section_settings.get("depth_um"), 

2987 scan_direction=tbt.FIBPatternScanDirection( 

2988 regular_cross_section_settings.get("scan_direction") 

2989 ), 

2990 scan_type=tbt.FIBPatternScanType( 

2991 regular_cross_section_settings.get("scan_type") 

2992 ), 

2993 ), 

2994 ) 

2995 # make sure application file is valid for this pattern type: 

2996 try: 

2997 geometry = pattern.geometry 

2998 microscope.patterning.set_default_application_file(pattern.application) 

2999 microscope.patterning.create_regular_cross_section( 

3000 center_x=geometry.center_um.x * Conversions.UM_TO_M, 

3001 center_y=geometry.center_um.y * Conversions.UM_TO_M, 

3002 width=geometry.width_um * Conversions.UM_TO_M, 

3003 height=geometry.height_um * Conversions.UM_TO_M, 

3004 depth=geometry.depth_um * Conversions.UM_TO_M, 

3005 ) 

3006 microscope.patterning.clear_patterns() 

3007 except: 

3008 raise ValueError( 

3009 f'Invalid application file of "{pattern.application}" for Regular Cross Section pattern type. Please select or create an appropriate application file.' 

3010 ) 

3011 return pattern 

3012 elif pattern_type == tbt.FIBPatternType.CLEANING_CROSS_SECTION: 

3013 validate_fib_box_settings( 

3014 settings=cleaning_cross_section_settings, 

3015 yml_format=yml_format, 

3016 step_name=step_name, 

3017 pattern_type=pattern_type, 

3018 ) 

3019 pattern = tbt.FIBPattern( 

3020 application=application_file, 

3021 type=pattern_type, 

3022 geometry=tbt.FIBCleaningCrossSection( 

3023 center_um=tbt.Point( 

3024 x=cleaning_cross_section_settings.get("center").get("x_um"), 

3025 y=cleaning_cross_section_settings.get("center").get("y_um"), 

3026 ), 

3027 width_um=cleaning_cross_section_settings.get("width_um"), 

3028 height_um=cleaning_cross_section_settings.get("height_um"), 

3029 depth_um=cleaning_cross_section_settings.get("depth_um"), 

3030 scan_direction=tbt.FIBPatternScanDirection( 

3031 cleaning_cross_section_settings.get("scan_direction") 

3032 ), 

3033 scan_type=tbt.FIBPatternScanType( 

3034 cleaning_cross_section_settings.get("scan_type") 

3035 ), 

3036 ), 

3037 ) 

3038 # make sure application file is valid for this pattern type: 

3039 try: 

3040 geometry = pattern.geometry 

3041 microscope.patterning.set_default_application_file(pattern.application) 

3042 microscope.patterning.create_cleaning_cross_section( 

3043 center_x=geometry.center_um.x * Conversions.UM_TO_M, 

3044 center_y=geometry.center_um.y * Conversions.UM_TO_M, 

3045 width=geometry.width_um * Conversions.UM_TO_M, 

3046 height=geometry.height_um * Conversions.UM_TO_M, 

3047 depth=geometry.depth_um * Conversions.UM_TO_M, 

3048 ) 

3049 microscope.patterning.clear_patterns() 

3050 except: 

3051 raise ValueError( 

3052 f'Invalid application file of "{pattern.application}" for Cleaning Cross Section pattern type. Please select or create an appropriate application file.' 

3053 ) 

3054 return pattern 

3055 elif pattern_type == tbt.FIBPatternType.SELECTED_AREA: 

3056 validate_fib_selected_area_settings( 

3057 settings=selected_area_settings, 

3058 yml_format=yml_format, 

3059 step_name=step_name, 

3060 pattern_type=pattern_type, 

3061 ) 

3062 pattern = tbt.FIBPattern( 

3063 application=application_file, 

3064 type=pattern_type, 

3065 geometry=tbt.FIBStreamPattern( 

3066 dwell_us=selected_area_settings.get("dwell_us"), 

3067 repeats=selected_area_settings.get("repeats"), 

3068 recipe=Path(selected_area_settings.get("recipe_file")), 

3069 mask=Path(selected_area_settings.get("mask_file")), 

3070 ), 

3071 ) 

3072 

3073 # make sure application file is valid for this pattern type: 

3074 try: 

3075 microscope.patterning.set_default_application_file(pattern.application) 

3076 microscope.patterning.create_rectangle( 

3077 center_x=0.0, 

3078 center_y=0.0, 

3079 width=10.0e-6, 

3080 height=10.0e-6, 

3081 depth=1.0e-6, 

3082 ) 

3083 microscope.patterning.clear_patterns() 

3084 except: 

3085 raise ValueError( 

3086 f'Invalid application file of "{pattern.application}" for Selected Area pattern. Please use an application file for Rectangle milling.' 

3087 ) 

3088 

3089 return pattern 

3090 else: 

3091 raise KeyError( 

3092 f"Invalid pattern type of {pattern_type}. Supported pattern types are: {[i.value for i in tbt.FIBPatternType]}" 

3093 ) 

3094 

3095 

3096def validate_fib_box_settings( 

3097 settings: dict, 

3098 yml_format: tbt.YMLFormatVersion, 

3099 step_name: str, 

3100 pattern_type: tbt.FIBPatternType, 

3101) -> bool: 

3102 """ 

3103 Perform schema checking for FIB box pattern setting dictionary. 

3104 

3105 This function validates the FIB box pattern settings dictionary based on the specified yml format. 

3106 

3107 ## Parameters 

3108 

3109 - `settings` (`dict`): The dictionary containing the FIB box pattern settings. 

3110 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

3111 - `step_name` (`str`): The name of the step in the .yml file. 

3112 - `pattern_type` (`tbt.FIBPatternType`): The type of the FIB pattern. 

3113 

3114 ## Returns 

3115 

3116 - `bool`: True if the settings are valid, False otherwise. 

3117 

3118 ## Raises 

3119 

3120 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

3121 """ 

3122 if yml_format.version >= 1.0: 3122 ↛ 3124,   3122 ↛ 31632 missed branches: 1) line 3122 didn't jump to line 3124 because the condition on line 3122 was never true, 2) line 3122 didn't jump to line 3163 because the condition on line 3122 was always true

3123 # flattens nested dictionary, adding "_" separator 

3124 flat_settings = ut._flatten(settings) 

3125 schema = Schema( 

3126 { 

3127 "center_x_um": And( 

3128 float, 

3129 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'x_um' parameter for pattern 'center' sub-dictionary must be a float. '{flat_settings['center_x_um']}' of type '{type(flat_settings['center_x_um'])}' was requested.", 

3130 ), 

3131 "center_y_um": And( 

3132 float, 

3133 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'y_um' parameter for pattern 'center' sub-dictionary must be a float. '{flat_settings['center_y_um']}' of type '{type(flat_settings['center_y_um'])}' was requested.", 

3134 ), 

3135 "width_um": And( 

3136 float, 

3137 lambda x: x > 0, 

3138 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'width_um' parameter must be a float. '{flat_settings['width_um']}' of type '{type(flat_settings['width_um'])}' was requested.", 

3139 ), 

3140 "height_um": And( 

3141 float, 

3142 lambda x: x > 0, 

3143 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'height_um' parameter must be a float. '{flat_settings['height_um']}' of type '{type(flat_settings['height_um'])}' was requested.", 

3144 ), 

3145 "depth_um": And( 

3146 float, 

3147 lambda x: x > 0, 

3148 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'depth_um' parameter must be a float. '{flat_settings['depth_um']}' of type '{type(flat_settings['depth_um'])}' was requested.", 

3149 ), 

3150 "scan_direction": And( 

3151 str, 

3152 lambda x: ut.valid_enum_entry(x, tbt.FIBPatternScanDirection), 

3153 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'scan_direction' parameter must be a valid scan direction type. '{flat_settings['scan_direction']}' was requested but supported values are: {[i.value for i in tbt.FIBPatternScanDirection]}", 

3154 ), 

3155 "scan_type": And( 

3156 str, 

3157 lambda x: ut.valid_enum_entry(x, tbt.FIBPatternScanType), 

3158 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'scan_type' parameter must be a valid scan type type. '{flat_settings['scan_type']}' was requested but supported values are: {[i.value for i in tbt.FIBPatternScanType]}", 

3159 ), 

3160 }, 

3161 ignore_extra_keys=True, 

3162 ) 

3163 try: 

3164 schema.validate(flat_settings) 

3165 except UnboundLocalError: 

3166 raise ValueError( 

3167 f"Error. Unsupported yml version {yml_format.version} provided." 

3168 ) 

3169 return True 

3170 

3171 

3172def validate_fib_selected_area_settings( 

3173 settings: dict, 

3174 yml_format: tbt.YMLFormatVersion, 

3175 step_name: str, 

3176 pattern_type: tbt.FIBPatternType, 

3177) -> bool: 

3178 """ 

3179 Perform schema checking for FIB selected area pattern setting dictionary. 

3180 

3181 This function validates the FIB selected area pattern settings dictionary based on the specified yml format. 

3182 

3183 ## Parameters 

3184 

3185 - `settings` (`dict`): The dictionary containing the FIB selected area pattern settings. 

3186 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

3187 - `step_name` (`str`): The name of the step in the .yml file. 

3188 - `pattern_type` (`tbt.FIBPatternType`): The type of the FIB pattern. 

3189 

3190 ## Returns 

3191 

3192 - `bool`: True if the settings are valid, False otherwise. 

3193 

3194 ## Raises 

3195 

3196 - `ValueError`: If the yml version is unsupported or if the settings do not satisfy the specified schema. 

3197 """ 

3198 if yml_format.version >= 1.0: 

3199 schema = Schema( 3199 ↛ exitline 3199 didn't jump to the function exit

3200 { 

3201 "dwell_us": And( 

3202 float, 

3203 lambda x: x > 0, 

3204 # check all float error versions of modulus 

3205 Or( 

3206 lambda x: math.isclose( 

3207 x % Constants.stream_pattern_base_dwell_us, 

3208 0, 

3209 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5, 

3210 ), 

3211 lambda x: math.isclose( 

3212 x % Constants.stream_pattern_base_dwell_us, 

3213 Constants.stream_pattern_base_dwell_us, 

3214 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5, 

3215 ), 

3216 lambda x: math.isclose( 

3217 x % Constants.stream_pattern_base_dwell_us, 

3218 -Constants.stream_pattern_base_dwell_us, 

3219 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5, 

3220 ), 

3221 ), 

3222 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'dwell_us' parameter must be a positive float and an integer multiple of the base dwell time, {Constants.stream_pattern_base_dwell_us * Conversions.US_TO_NS} ns. '{settings['dwell_us']}' us of type '{type(settings['dwell_us'])}' was requested.", 

3223 ), 

3224 "repeats": And( 

3225 int, 

3226 lambda x: x > 0, 

3227 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'repeats' parameter must be a positive integer. '{settings['repeats']}' of type '{type(settings['repeats'])}' was requested.", 

3228 ), 

3229 "recipe_file": And( 

3230 str, 

3231 lambda x: Path(x).is_file(), 

3232 lambda x: Path(x).suffix == ".py", 

3233 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'recipe_file' parameter must be a valid file path string with the extension '.py' and must already exist. The recipe file '{settings['recipe_file']}' was requested.", 

3234 ), 

3235 "mask_file": And( 

3236 str, 

3237 lambda x: Path(x).suffix == ".tif", 

3238 error=f"In 'fib' step_type for step '{step_name}' and pattern type '{pattern_type.value}', 'mask' parameter must be a file path string with a file extension of '.tif'. '{settings['mask_file']}' of type '{type(settings['mask_file'])}' was requested.", 

3239 ), 

3240 }, 

3241 ignore_extra_keys=True, 

3242 ) 

3243 try: 

3244 schema.validate(settings) 

3245 except UnboundLocalError: 

3246 raise ValueError( 

3247 f"Error. Unsupported yml version {yml_format.version} provided." 

3248 ) 

3249 return True 

3250 

3251 

3252def step( 

3253 microscope: tbt.Microscope, 

3254 # slice_number: str, 

3255 step_name: str, 

3256 step_settings: dict, 

3257 general_settings: tbt.GeneralSettings, 

3258 yml_format: tbt.YMLFormatVersion, 

3259) -> tbt.Step: 

3260 """ 

3261 Create a step object for different step types, including validation. 

3262 

3263 This function creates a `Step` object for the specified step type and performs validation. 

3264 

3265 ## Parameters 

3266 

3267 - `microscope` (`tbt.Microscope`): The microscope object for which to create the step. 

3268 - `step_name` (`str`): The name of the step in the .yml file. 

3269 - `step_settings` (`dict`): The dictionary containing the step settings. 

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

3271 - `yml_format` (`tbt.YMLFormatVersion`): The format specified by the version of the .yml file. 

3272 

3273 ## Returns 

3274 

3275 - `tbt.Step`: The step object. 

3276 

3277 ## Raises 

3278 

3279 - `NotImplementedError`: If the step type is unsupported. 

3280 - `KeyError`: If required settings are missing or invalid. 

3281 """ 

3282 

3283 # parsing settings 

3284 step_type_value = step_settings[yml_format.step_general_key][ 

3285 yml_format.step_type_key 

3286 ] 

3287 if not ut.valid_enum_entry(step_type_value, tbt.StepType): 

3288 raise NotImplementedError( 

3289 f"Unsupported step type of '{step_type_value}', for step name '{step_name}' supported types are: {[i.value for i in tbt.StepType]}." 

3290 ) 

3291 step_type = tbt.StepType(step_type_value) 

3292 

3293 step_number = step_settings[yml_format.step_general_key][yml_format.step_number_key] 

3294 if not isinstance(step_number, int) or (step_number < 1): 

3295 raise KeyError( 

3296 f"Invalid step number of '{step_number}', for step name '{step_name}'. Must be a positive integer greater than 0." 

3297 ) 

3298 step_frequency = step_settings[yml_format.step_general_key][ 

3299 yml_format.step_frequency_key 

3300 ] 

3301 if not isinstance(step_frequency, int) or (step_frequency < 1): 3301 ↛ 3305line 3301 didn't jump to line 3305 because the condition on line 3301 was always true

3302 raise KeyError( 

3303 f"Invalid step frequency of '{step_frequency}', for step name '{step_name}'. Must be a positive integer greater than 0." 

3304 ) 

3305 stage_db = step_settings[yml_format.step_general_key][ 

3306 yml_format.step_stage_settings_key 

3307 ] 

3308 

3309 # check and validate stage 

3310 stage_settings = stage_position_settings( 

3311 microscope=microscope, 

3312 step_name=step_name, 

3313 general_settings=general_settings, 

3314 step_stage_settings=stage_db, 

3315 yml_format=yml_format, 

3316 ) 

3317 

3318 # TODO 

3319 # operation_settings, could use match statement in python >= 3.10 

3320 if step_type == tbt.StepType.EBSD: 

3321 operation_settings = ebsd( 

3322 microscope=microscope, 

3323 step_settings=step_settings, 

3324 step_name=step_name, 

3325 yml_format=yml_format, 

3326 ) 

3327 if step_type == tbt.StepType.EDS: 

3328 operation_settings = eds( 

3329 microscope=microscope, 

3330 step_settings=step_settings, 

3331 step_name=step_name, 

3332 yml_format=yml_format, 

3333 ) 

3334 if step_type == tbt.StepType.IMAGE: 

3335 operation_settings = image( 

3336 microscope=microscope, 

3337 step_settings=step_settings, 

3338 step_name=step_name, 

3339 yml_format=yml_format, 

3340 ) 

3341 if step_type == tbt.StepType.LASER: 

3342 operation_settings = laser( 

3343 microscope=microscope, 

3344 step_settings=step_settings, 

3345 step_name=step_name, 

3346 yml_format=yml_format, 

3347 ) 

3348 if step_type == tbt.StepType.CUSTOM: 

3349 operation_settings = custom( 

3350 microscope=microscope, 

3351 step_settings=step_settings, 

3352 step_name=step_name, 

3353 yml_format=yml_format, 

3354 ) 

3355 if step_type == tbt.StepType.FIB: 3355 ↛ 3363line 3355 didn't jump to line 3363 because the condition on line 3355 was always true

3356 operation_settings = fib( 

3357 microscope=microscope, 

3358 step_settings=step_settings, 

3359 step_name=step_name, 

3360 yml_format=yml_format, 

3361 ) 

3362 

3363 step_object = tbt.Step( 

3364 type=step_type, 

3365 name=step_name, 

3366 number=step_number, 

3367 frequency=step_frequency, 

3368 stage=stage_settings, 

3369 operation_settings=operation_settings, 

3370 ) 

3371 

3372 return step_object