Coverage for src\pytribeam\factory.py: 73%

709 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2025-03-04 17:41 -0800

1## python standard libraries 

2from pathlib import Path 

3import platform 

4import time 

5from typing import NamedTuple, List, Union 

6import warnings 

7from functools import singledispatch 

8import math 

9 

10 

11# 3rd party libraries 

12import pytest 

13from schema import And, Or, Schema, SchemaError 

14import jsonschema 

15 

16# Local 

17import pytribeam.insertable_devices as devices 

18import pytribeam.image as img 

19import pytribeam.utilities as ut 

20import pytribeam.stage as stage 

21from pytribeam.constants import Conversions, Constants 

22from pytribeam.fib import application_files 

23 

24try: 

25 import pytribeam.laser as fs_laser 

26except: 

27 pass 

28import pytribeam.types as tbt 

29 

30 

31def active_fib_applications( 

32 microscope: tbt.Microscope, 

33) -> list: 

34 return microscope.patterning.list_all_application_files() 

35 

36 

37def active_beam_with_settings( 

38 microscope: tbt.Microscope, 

39) -> tbt.Beam: 

40 """Beam factory 

41 This will grab current beam and its settings on the microscope to make a beam object. These settings will fully depend on the currently active beam as determined by xTUI. Tolerance values for voltage and current will be autopopulated as a ratio of current values predetermined in the constants.Constants() class. 

42 """ 

43 selected_beam = active_imaging_device(microscope=microscope) 

44 beam = ut.beam_type(selected_beam, microscope) 

45 

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

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

48 

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

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

51 

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

53 

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

55 

56 active_settings = tbt.BeamSettings( 

57 voltage_kv=voltage_kv, 

58 current_na=current_na, 

59 hfw_mm=hfw_mm, 

60 working_dist_mm=working_dist_mm, 

61 voltage_tol_kv=voltage_tol_kv, 

62 current_tol_na=current_tol_na, 

63 ) 

64 

65 return type(selected_beam)(settings=active_settings) 

66 

67 

68def active_detector_settings( 

69 microscope: tbt.Microscope, 

70) -> tbt.Detector: 

71 """Detector factory 

72 This will grab current detector settings on the microscope to make a detector object. These settings will fully depend on the currently active detector as determined by xTUI. 

73 """ 

74 

75 detector_type = microscope.detector.type.value 

76 detector_mode = microscope.detector.mode.value 

77 brightness = microscope.detector.brightness.value 

78 contrast = microscope.detector.contrast.value 

79 auto_cb_settings = tbt.ScanArea( 

80 left=None, 

81 top=None, 

82 width=None, 

83 height=None, 

84 ) 

85 custom_settings = None 

86 

87 active_detector = tbt.Detector( 

88 type=detector_type, 

89 mode=detector_mode, 

90 brightness=brightness, 

91 contrast=contrast, 

92 auto_cb_settings=auto_cb_settings, 

93 custom_settings=custom_settings, 

94 ) 

95 

96 return active_detector 

97 

98 

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

100 """""" 

101 beam = active_beam_with_settings(microscope=microscope) 

102 detector = active_detector_settings(microscope=microscope) 

103 scan = active_scan_settings(microscope=microscope) 

104 bit_depth = Constants.default_color_depth 

105 

106 active_image_settings = tbt.ImageSettings( 

107 microscope=microscope, 

108 beam=beam, 

109 detector=detector, 

110 scan=scan, 

111 bit_depth=bit_depth, 

112 ) 

113 

114 return active_image_settings 

115 

116 

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

118 """Determines active imaging device and returns correct internal beam type object with null beam settings""" 

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

120 if curr_device == tbt.Device.ELECTRON_BEAM: 

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

122 settings=tbt.BeamSettings() 

123 ) 

124 elif curr_device == tbt.Device.ION_BEAM: 

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

126 settings=tbt.BeamSettings() 

127 ) 

128 else: 

129 raise ValueError( 

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

131 ) 

132 return selected_beam 

133 

134 

135def active_scan_settings( 

136 microscope: tbt.Microscope, 

137) -> tbt.Scan: 

138 """Scan factory 

139 This will grab current scan settings on the microscope to make a scan object. These settings will fully depend on the currently active scan settings as determined by xTUI. 

140 """ 

141 selected_beam = active_imaging_device(microscope=microscope) 

142 beam = ut.beam_type(selected_beam, microscope) 

143 

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

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

146 current_res = beam.scanning.resolution.value 

147 res = string_to_res(current_res) 

148 resolution = tbt.PresetResolution(res) 

149 

150 active_scan = tbt.Scan( 

151 rotation_deg=rotation_deg, 

152 dwell_time_us=dwell_time_us, 

153 resolution=resolution, 

154 # mode=mode, 

155 ) 

156 

157 return active_scan 

158 

159 

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

161 """Returns current stage position in raw coordinate system and user units [mm, deg]""" 

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

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

164 direct_encoder_pos = microscope.specimen.stage.current_position 

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

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

167 coord_system_str = direct_encoder_pos.coordinate_system 

168 

169 encoder_pos = tbt.StagePositionEncoder( 

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

171 ) 

172 user_pos = stage.encoder_to_user_position(encoder_pos) 

173 

174 # ensure r-axis is kept in axis limit 

175 if not ut.in_interval( 

176 val=user_pos.r_deg, 

177 limit=Constants.rotation_axis_limit_deg, 

178 type=tbt.IntervalType.RIGHT_OPEN, 

179 ): 

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

181 while user_pos.r_deg >= Constants.rotation_axis_limit_deg.max: 

182 new_r_deg = user_pos.r_deg - 360.0 

183 while user_pos.r_deg < Constants.rotation_axis_limit_deg.min: 

184 new_r_deg = user_pos.r_deg + 360.0 

185 new_r_deg = round(new_r_deg, 6) 

186 user_pos = tbt.StagePositionUser( 

187 x_mm=user_pos.x_mm, 

188 y_mm=user_pos.y_mm, 

189 z_mm=user_pos.z_mm, 

190 r_deg=new_r_deg, 

191 t_deg=user_pos.t_deg, 

192 ) 

193 

194 return user_pos 

195 

196 

197def active_laser_state() -> tbt.LaserState: 

198 """returns dictionary object for all properties that can be quickly read from the laser (not exhaustive) 

199 Power can be read but has its own method and is more involved. 

200 Flipper configuration can only be set, not read""" 

201 vals = fs_laser.tfs_laser.Laser_ReadValues() 

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

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

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

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

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

207 vals["expected_pattern_duration_s"] = ( 

208 fs_laser.tfs_laser.Patterning_GetExpectedDuration() 

209 ) 

210 

211 pattern_db = vals["pattern"] 

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

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

214 raise KeyError( 

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

216 ) 

217 pattern_type = tbt.LaserPatternType(pattern_type) 

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

219 if mode == tbt.LaserPatternMode.COARSE: 

220 pixel_dwell_ms = pattern_db["dwellTime"] 

221 pulses_per_pixel = None 

222 elif mode == tbt.LaserPatternMode.FINE: 

223 pixel_dwell_ms = None 

224 pulses_per_pixel = pattern_db["pulsesPerPixel"] 

225 rotation_deg = pattern_db["patternRotation_deg"] 

226 

227 if pattern_type == tbt.LaserPatternType.BOX: 

228 geometry = tbt.LaserBoxPattern( 

229 passes=pattern_db["passes"], 

230 size_x_um=pattern_db["xSize_um"], 

231 size_y_um=pattern_db["ySize_um"], 

232 pitch_x_um=pattern_db["xPitch_um"], 

233 pitch_y_um=pattern_db["yPitch_um"], 

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

235 coordinate_ref=tbt.CoordinateReference( 

236 pattern_db["coordReference"].lower() 

237 ), 

238 ) 

239 if pattern_type == tbt.LaserPatternType.LINE: 

240 geometry = tbt.LaserLinePattern( 

241 passes=pattern_db["passes"], 

242 size_um=pattern_db["xSize_um"], 

243 pitch_um=pattern_db["xPitch_um"], 

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

245 ) 

246 

247 pattern = tbt.LaserPattern( 

248 mode=mode, 

249 rotation_deg=rotation_deg, 

250 geometry=geometry, 

251 pulses_per_pixel=pulses_per_pixel, 

252 pixel_dwell_ms=pixel_dwell_ms, 

253 ) 

254 

255 state = tbt.LaserState( 

256 wavelength_nm=vals["wavelength_nm"], 

257 frequency_khz=vals["frequency_kHz"], 

258 pulse_divider=vals["pulse_divider"], 

259 pulse_energy_uj=vals["pulse_energy_uJ"], 

260 objective_position_mm=vals["objective_position_mm"], 

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

262 pattern=pattern, 

263 expected_pattern_duration_s=vals["expected_pattern_duration_s"], 

264 ) 

265 

266 return state 

267 

268 

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

270 """Laser Settings factory. Some values cannot be read by Laser Control, only set. For example, Polarization will default to "Vertical" as this value cannot be read""" 

271 state = active_laser_state() 

272 

273 settings = tbt.LaserSettings( 

274 microscope=microscope, 

275 pulse=tbt.LaserPulse( 

276 wavelength_nm=state.wavelength_nm, 

277 divider=state.pulse_divider, 

278 energy_uj=state.pulse_energy_uj, 

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

280 ), 

281 objective_position_mm=state.objective_position_mm, 

282 beam_shift_um=state.beam_shift_um, 

283 pattern=state.pattern, 

284 ) 

285 

286 return settings 

287 

288 

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

290 """Returns available detector types on the current microscope""" 

291 detectors = microscope.detector.type.available_values 

292 # available = [tbt.DetectorType(i) for i in detectors] 

293 return detectors 

294 

295 

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

297 """Returns available detector modes on the current microscope""" 

298 modes = microscope.detector.mode.available_values 

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

300 return modes 

301 

302 

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

304 if type.value == "electron": 

305 return tbt.ElectronBeam 

306 if type.value == "ion": 

307 return tbt.IonBeam 

308 raise NotImplementedError(f"Unsupported beam type {type.value}") 

309 

310 

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

312 """stage limits from current microscope connection""" 

313 stage.coordinate_system(microscope=microscope) 

314 # x position 

315 min_x_mm = ( 

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

317 * Conversions.M_TO_MM 

318 ) 

319 max_x_mm = ( 

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

321 * Conversions.M_TO_MM 

322 ) 

323 # y position 

324 min_y_mm = ( 

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

326 * Conversions.M_TO_MM 

327 ) 

328 max_y_mm = ( 

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

330 * Conversions.M_TO_MM 

331 ) 

332 # z position 

333 min_z_mm = ( 

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

335 * Conversions.M_TO_MM 

336 ) 

337 max_z_mm = ( 

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

339 * Conversions.M_TO_MM 

340 ) 

341 # r position 

342 min_r_deg = Constants.rotation_axis_limit_deg.min 

343 max_r_deg = Constants.rotation_axis_limit_deg.max 

344 # t position 

345 min_t_deg = ( 

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

347 * Conversions.RAD_TO_DEG 

348 ) 

349 max_t_deg = ( 

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

351 * Conversions.RAD_TO_DEG 

352 ) 

353 

354 return tbt.StageLimits( 

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

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

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

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

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

360 ) 

361 

362 

363def beam_limits( 

364 selected_beam: property, 

365 beam_type: tbt.BeamType, 

366) -> tbt.BeamLimits: 

367 # voltage range 

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

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

370 

371 # current range 

372 if beam_type == tbt.BeamType.ELECTRON: 

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

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

375 if beam_type == tbt.BeamType.ION: 

376 available_currents = selected_beam.beam_current.available_values 

377 min_na = min(available_currents) * Conversions.A_TO_NA 

378 max_na = max(available_currents) * Conversions.A_TO_NA 

379 

380 # hfw range 

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

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

383 

384 # working_dist range 

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

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

387 

388 return tbt.BeamLimits( 

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

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

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

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

393 ) 

394 

395 

396def general( 

397 general_db: dict, 

398 yml_format: tbt.YMLFormatVersion, 

399) -> tbt.GeneralSettings: 

400 """Converts general setting dictionary to built-in type. 

401 Performs schema checking to ensure valid inputs are requested. 

402 

403 Args: 

404 general_db: general settings dictioanry from .yml 

405 yml_format: format specified by version of the .yml file 

406 

407 Returns: 

408 general settings 

409 """ 

410 

411 if not yml_format in tbt.YMLFormatVersion: 

412 raise NotImplementedError( 

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

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

415 ) 

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

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

418 

419 validate_general_settings( 

420 settings=general_db, 

421 yml_format=yml_format, 

422 ) 

423 

424 if yml_format.version >= 1.0: 

425 # slice thickness 

426 slice_thickness_um = general_db["slice_thickness_um"] 

427 # max slice number 

428 max_slice_number = general_db["max_slice_num"] 

429 # pre tilt 

430 pre_tilt_deg = general_db["pre_tilt_deg"] 

431 # sectioning axis 

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

433 # stage tolerance 

434 stage_tolerance = tbt.StageTolerance( 

435 translational_um=general_db["stage_translational_tol_um"], 

436 angular_deg=general_db["stage_angular_tol_deg"], 

437 ) 

438 # connection 

439 connection = tbt.MicroscopeConnection( 

440 general_db["connection_host"], general_db["connection_port"] 

441 ) 

442 # EBSD OEM 

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

444 # EDS OEM 

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

446 # exp dir 

447 exp_dir = general_db["exp_dir"] 

448 # h5 log name 

449 h5_log_name = general_db["h5_log_name"] 

450 # remove log file extension if the user provided it 

451 log_extension = Constants.logfile_extension 

452 if h5_log_name.endswith(log_extension): 

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

454 # step count 

455 step_count = general_db["step_count"] 

456 yml_version = 1.0 

457 

458 general_settings = tbt.GeneralSettings( 

459 yml_version=yml_version, 

460 slice_thickness_um=slice_thickness_um, 

461 max_slice_number=max_slice_number, 

462 pre_tilt_deg=pre_tilt_deg, 

463 sectioning_axis=sectioning_axis, 

464 stage_tolerance=stage_tolerance, 

465 connection=connection, 

466 EBSD_OEM=ebsd_oem, 

467 EDS_OEM=eds_oem, 

468 exp_dir=Path(exp_dir), 

469 h5_log_name=h5_log_name, 

470 step_count=step_count, 

471 ) 

472 

473 return general_settings 

474 

475 

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

477 # if settings is None or ut.none_value_dictionary(dictionary=settings): 

478 # return tbt.LaserBoxPattern() 

479 return tbt.LaserBoxPattern( 

480 passes=settings["passes"], 

481 size_x_um=settings["size_x_um"], 

482 size_y_um=settings["size_y_um"], 

483 pitch_x_um=settings["pitch_x_um"], 

484 pitch_y_um=settings["pitch_y_um"], 

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

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

487 ) 

488 

489 

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

491 # if settings is None or ut.none_value_dictionary(dictionary=settings): 

492 # return tbt.LaserLinePattern() 

493 return tbt.LaserLinePattern( 

494 passes=settings["passes"], 

495 size_um=settings["size_um"], 

496 pitch_um=settings["pitch_um"], 

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

498 ) 

499 

500 

501def laser( 

502 microscope: tbt.Microscope, 

503 step_settings: dict, 

504 step_name: str, 

505 yml_format: tbt.YMLFormatVersion, 

506) -> tbt.LaserSettings: 

507 """Coverts laser step from .yml to microscope settings to perform laser milling. Performs schema checking to ensure valid inputs are requested""" 

508 if yml_format.version >= 1.0: 

509 # pulse settings 

510 pulse_set_db = step_settings.get("pulse") 

511 if pulse_set_db is None: 

512 raise KeyError( 

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

514 ) 

515 # laser optics settings 

516 optics_set_db = { 

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

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

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

520 } 

521 # pattern settings 

522 pattern_set_db = step_settings.get("pattern") 

523 if pattern_set_db is None: 

524 raise KeyError( 

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

526 ) 

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

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

529 

530 validate_pulse_settings( 

531 settings=pulse_set_db, 

532 yml_format=yml_format, 

533 step_name=step_name, 

534 ) 

535 pulse = tbt.LaserPulse( 

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

537 divider=pulse_set_db["divider"], 

538 energy_uj=pulse_set_db["energy_uj"], 

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

540 ) 

541 

542 validate_laser_optics_settings( 

543 settings=optics_set_db, 

544 yml_format=yml_format, 

545 step_name=step_name, 

546 ) 

547 

548 pattern_type = validate_laser_pattern_settings( 

549 settings=pattern_set_db, 

550 yml_format=yml_format, 

551 step_name=step_name, 

552 ) 

553 if pattern_type == tbt.LaserPatternType.BOX: 

554 geometry = laser_box_pattern(box_pattern_db) 

555 if pattern_type == tbt.LaserPatternType.LINE: 

556 geometry = laser_line_pattern(line_pattern_db) 

557 pattern = tbt.LaserPattern( 

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

559 rotation_deg=pattern_set_db["rotation_deg"], 

560 pulses_per_pixel=pattern_set_db["pulses_per_pixel"], 

561 pixel_dwell_ms=pattern_set_db["pixel_dwell_ms"], 

562 geometry=geometry, 

563 ) 

564 

565 laser_settings = tbt.LaserSettings( 

566 microscope=microscope, 

567 pulse=pulse, 

568 objective_position_mm=optics_set_db["objective_position_mm"], 

569 beam_shift_um=tbt.Point( 

570 x=optics_set_db["beam_shift_um_x"], 

571 y=optics_set_db["beam_shift_um_y"], 

572 ), 

573 pattern=pattern, 

574 ) 

575 

576 return laser_settings 

577 

578 

579def image( 

580 microscope: tbt.Microscope, 

581 step_settings: dict, 

582 step_name: str, 

583 yml_format: tbt.YMLFormatVersion, 

584) -> tbt.ImageSettings: 

585 """Converts image step from .yml file to microscope settings 

586 to capture an image. Performs schema checking to ensure valid 

587 inputs are requested. 

588 

589 Args: 

590 microscope: the connection to the microscope 

591 step_settings: image step dictioanry from a .yml file 

592 yml_format: format specified by version of the .yml file 

593 

594 Returns: 

595 image settings 

596 """ 

597 if yml_format.version >= 1.0: 

598 step_general = step_settings.get(yml_format.step_general_key) 

599 if step_general is None: 

600 raise KeyError( 

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

602 ) 

603 step_type = step_general.get(yml_format.step_type_key) 

604 if step_type is None: 

605 raise KeyError( 

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

607 ) 

608 # beam settings 

609 beam_set_db = step_settings.get("beam") 

610 if beam_set_db is None: 

611 raise KeyError( 

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

613 ) 

614 beam_type_value = beam_set_db.get("type") 

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

616 raise NotImplementedError( 

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

618 ) 

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

620 

621 # detector settings 

622 detector_set_db = step_settings.get("detector") 

623 if detector_set_db is None: 

624 raise KeyError( 

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

626 ) 

627 auto_cb_set_db = detector_set_db.get("auto_cb") 

628 

629 # scan settings 

630 scan_set_db = step_settings.get("scan") 

631 if scan_set_db is None: 

632 raise KeyError( 

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

634 ) 

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

636 

637 # misc settings 

638 bit_depth = step_settings.get("bit_depth") 

639 

640 # TODO incorporate tile settings 

641 if yml_format.version >= 1.1: 

642 # tile settings 

643 tile_set_db = step_settings.get("tile_settings") 

644 

645 validate_beam_settings( 

646 microscope=microscope, 

647 beam_type=beam_type, 

648 settings=beam_set_db, 

649 yml_format=yml_format, 

650 step_name=step_name, 

651 ) 

652 beam_settings = tbt.BeamSettings( 

653 voltage_kv=beam_set_db["voltage_kv"], 

654 voltage_tol_kv=beam_set_db["voltage_tol_kv"], 

655 current_na=beam_set_db["current_na"], 

656 current_tol_na=beam_set_db["current_tol_na"], 

657 hfw_mm=beam_set_db["hfw_mm"], 

658 working_dist_mm=beam_set_db["working_dist_mm"], 

659 dynamic_focus=beam_set_db["dynamic_focus"], 

660 tilt_correction=beam_set_db["tilt_correction"], 

661 ) 

662 

663 validate_auto_cb_settings( 

664 settings=auto_cb_set_db, 

665 yml_format=yml_format, 

666 step_name=step_name, 

667 ) 

668 auto_cb_settings = tbt.ScanArea( 

669 left=auto_cb_set_db["left"], 

670 top=auto_cb_set_db["top"], 

671 width=auto_cb_set_db["width"], 

672 height=auto_cb_set_db["height"], 

673 ) 

674 

675 validate_detector_settings( 

676 microscope=microscope, 

677 beam_type=beam_type, 

678 settings=detector_set_db, 

679 yml_format=yml_format, 

680 step_name=step_name, 

681 ) 

682 detector_settings = tbt.Detector( 

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

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

685 brightness=detector_set_db["brightness"], 

686 contrast=detector_set_db["contrast"], 

687 auto_cb_settings=auto_cb_settings, 

688 # custom_settings=None, 

689 ) 

690 

691 validate_scan_settings( 

692 microscope=microscope, 

693 beam_type=beam_type, 

694 settings=scan_set_db, 

695 yml_format=yml_format, 

696 step_name=step_name, 

697 ) 

698 

699 # cast resolution to preset if applicable 

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

701 scan_res = tbt.PresetResolution(scan_res) 

702 

703 scan_settings = tbt.Scan( 

704 rotation_deg=scan_set_db["rotation_deg"], 

705 dwell_time_us=scan_set_db["dwell_time_us"], 

706 resolution=scan_res, 

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

708 ) 

709 

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

711 if beam_settings.dynamic_focus or beam_settings.tilt_correction: 

712 if not math.isclose(a=scan_settings.rotation_deg, b=0.0): 

713 raise ValueError( 

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

715 ) 

716 

717 # validate bit_depth 

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

719 valid_bit_depths = [i.value for i in tbt.ColorDepth] 

720 raise ValueError( 

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

722 ) 

723 

724 image_settings = tbt.ImageSettings( 

725 microscope=microscope, 

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

727 detector=detector_settings, 

728 scan=scan_settings, 

729 bit_depth=bit_depth, 

730 ) 

731 

732 return image_settings 

733 

734 

735def fib( 

736 microscope: tbt.Microscope, 

737 step_settings: dict, 

738 step_name: str, 

739 yml_format: tbt.YMLFormatVersion, 

740) -> tbt.FIBSettings: 

741 """Converts fib step from .yml file to microscope settings 

742 to perform a fib operation. Performs schema checking to ensure valid inputs are requested. 

743 """ 

744 ## create image_step_settings from this 

745 if yml_format.version >= 1.0: 

746 image_step_settings = step_settings.get("image") 

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

748 

749 mill_step_settings = step_settings.get("mill") 

750 mill_beam_db = mill_step_settings.get("beam") 

751 mill_pattern_db = mill_step_settings.get("pattern") 

752 

753 # ensure image is with an ion beam 

754 enforce_beam_type( 

755 tbt.IonBeam(settings=None), 

756 step_settings=image_step_settings, 

757 step_name=step_name, 

758 yml_format=yml_format, 

759 ) 

760 image_settings = image( 

761 microscope=microscope, 

762 step_settings=image_step_settings, 

763 step_name=step_name, 

764 yml_format=yml_format, 

765 ) 

766 

767 ## fib mill settings 

768 # ensure milling is with an ion beam 

769 enforce_beam_type( 

770 tbt.IonBeam(settings=None), 

771 step_settings=mill_step_settings, 

772 step_name=step_name, 

773 yml_format=yml_format, 

774 ) 

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

776 # mill beam 

777 validate_beam_settings( 

778 microscope=microscope, 

779 beam_type=beam_type, 

780 settings=mill_beam_db, 

781 yml_format=yml_format, 

782 step_name=step_name, 

783 ) 

784 mill_beam = tbt.IonBeam( 

785 settings=tbt.BeamSettings( 

786 voltage_kv=mill_beam_db["voltage_kv"], 

787 voltage_tol_kv=mill_beam_db["voltage_tol_kv"], 

788 current_na=mill_beam_db["current_na"], 

789 current_tol_na=mill_beam_db["current_tol_na"], 

790 hfw_mm=mill_beam_db["hfw_mm"], 

791 working_dist_mm=mill_beam_db["working_dist_mm"], 

792 dynamic_focus=mill_beam_db["dynamic_focus"], 

793 tilt_correction=mill_beam_db["tilt_correction"], 

794 ) 

795 ) 

796 

797 # fib pattern settings 

798 pattern = validate_fib_pattern_settings( 

799 microscope=microscope, 

800 settings=mill_pattern_db, 

801 yml_format=yml_format, 

802 step_name=step_name, 

803 ) 

804 

805 fib_settings = tbt.FIBSettings( 

806 microscope=microscope, 

807 image=image_settings, 

808 mill_beam=mill_beam, 

809 pattern=pattern, 

810 ) 

811 return fib_settings 

812 

813 

814@singledispatch 

815def enforce_beam_type( 

816 beam_type, 

817 step_settings: dict, 

818 step_name: str, 

819 yml_format: tbt.YMLFormatVersion, 

820) -> bool: 

821 """Enforce a specific beam type is used for an operation based on a dictionary. Dictionary must contain sub-dictionary with key 'beam'""" 

822 _ = beam_type 

823 __ = step_settings 

824 ___ = step_name 

825 ____ = yml_format 

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

827 

828 

829@enforce_beam_type.register 

830def _( 

831 beam_type: tbt.ElectronBeam, 

832 step_settings: dict, 

833 step_name: str, 

834 yml_format: tbt.YMLFormatVersion, 

835) -> bool: 

836 # beam must be electron 

837 if yml_format.version >= 1.0: 

838 beam_set_db = step_settings.get("beam") 

839 if beam_set_db is None: 

840 raise KeyError( 

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

842 ) 

843 beam_type_value = beam_set_db.get("type") 

844 

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

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

847 raise NotImplementedError(electron_beam_error_message) 

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

849 raise NotImplementedError(electron_beam_error_message) 

850 

851 

852@enforce_beam_type.register 

853def _( 

854 beam_type: tbt.IonBeam, 

855 step_settings: dict, 

856 step_name: str, 

857 yml_format: tbt.YMLFormatVersion, 

858) -> bool: 

859 # beam must be ion 

860 if yml_format.version >= 1.0: 

861 beam_set_db = step_settings.get("beam") 

862 if beam_set_db is None: 

863 raise KeyError( 

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

865 ) 

866 beam_type_value = beam_set_db.get("type") 

867 

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

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

870 raise NotImplementedError(ion_beam_error_message) 

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

872 raise NotImplementedError(ion_beam_error_message) 

873 

874 

875def ebsd( 

876 microscope: tbt.Microscope, 

877 step_settings: dict, 

878 step_name: str, 

879 yml_format: tbt.YMLFormatVersion, 

880) -> tbt.EBSDSettings: 

881 """Performs schema checking to ensure valid inputs are requested.""" 

882 enforce_beam_type( 

883 tbt.ElectronBeam(settings=None), 

884 step_settings=step_settings, 

885 step_name=step_name, 

886 yml_format=yml_format, 

887 ) 

888 image_settings = image( 

889 microscope=microscope, 

890 step_settings=step_settings, 

891 step_name=step_name, 

892 yml_format=yml_format, 

893 ) 

894 concurrent_EDS = step_settings.get("concurrent_EDS") 

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

896 enable_eds = False 

897 elif concurrent_EDS == True: 

898 enable_eds = True 

899 else: 

900 raise KeyError( 

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

902 ) 

903 ebsd_settings = tbt.EBSDSettings( 

904 image=image_settings, 

905 enable_eds=enable_eds, 

906 enable_ebsd=True, 

907 ) 

908 return ebsd_settings 

909 

910 

911def eds( 

912 microscope: tbt.Microscope, 

913 step_settings: dict, 

914 step_name: str, 

915 yml_format: tbt.YMLFormatVersion, 

916) -> tbt.EDSSettings: 

917 """Performs schema checking to ensure valid inputs are requested.""" 

918 enforce_beam_type( 

919 tbt.ElectronBeam(settings=None), 

920 step_settings=step_settings, 

921 step_name=step_name, 

922 yml_format=yml_format, 

923 ) 

924 image_settings = image( 

925 microscope=microscope, 

926 step_settings=step_settings, 

927 step_name=step_name, 

928 yml_format=yml_format, 

929 ) 

930 eds_settings = tbt.EDSSettings( 

931 image=image_settings, 

932 enable_eds=True, 

933 ) 

934 return eds_settings 

935 

936 

937# def ebsd_eds( 

938# microscope: tbt.Microscope, 

939# step_settings: dict, 

940# step_name: str, 

941# yml_format: tbt.YMLFormatVersion, 

942# ) -> tbt.EDSSettings: 

943# """Performs schema checking to ensure valid inputs are requested.""" 

944# enforce_beam_type( 

945# tbt.ElectronBeam(settings=None), 

946# step_settings=step_settings, 

947# step_name=step_name, 

948# yml_format=yml_format, 

949# ) 

950# image_settings = image( 

951# microscope=microscope, 

952# step_settings=step_settings, 

953# step_name=step_name, 

954# yml_format=yml_format, 

955# ) 

956# ebsd_eds_settings = tbt.EBSD_EDSSettings( 

957# image=image_settings, 

958# enable_ebsd=True, 

959# enable_eds=True, 

960# ) 

961# return ebsd_eds_settings 

962 

963 

964def custom( 

965 microscope: tbt.Microscope, 

966 step_settings: dict, 

967 step_name: str, 

968 yml_format: tbt.YMLFormatVersion, 

969) -> tbt.CustomSettings: 

970 """Performs schema checking to ensure valid inputs are requested.""" 

971 if yml_format.version >= 1.0: 

972 script_path = step_settings.get("script_path") 

973 if script_path is None: 

974 raise KeyError( 

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

976 ) 

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

978 raise ValueError( 

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

980 ) 

981 

982 executable_path = step_settings.get("executable_path") 

983 if executable_path is None: 

984 raise KeyError( 

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

986 ) 

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

988 raise ValueError( 

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

990 ) 

991 

992 custom_settings = tbt.CustomSettings( 

993 script_path=Path(script_path), 

994 executable_path=Path(executable_path), 

995 ) 

996 return custom_settings 

997 

998 

999def fib_pattern_type( 

1000 settings: tbt.FIBSettings, 

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

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

1003 

1004 

1005def scan_limits( 

1006 selected_beam: property, 

1007) -> tbt.ScanLimits: 

1008 """Scan settings limits""" 

1009 # rotation 

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

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

1012 # dwell_time 

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

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

1015 

1016 return tbt.ScanLimits( 

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

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

1019 ) 

1020 

1021 

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

1023 """Converts string in format of "{{width}}x{{height}}" to resolution object""" 

1024 try: 

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

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

1027 except: 

1028 raise ValueError( 

1029 f"""Invalid string format,  

1030 expected string format of "WIDTHxHEIGHT",  

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

1032 ) 

1033 

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

1035 

1036 

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

1038 """validates string resolution""" 

1039 res = string_to_res(string_resolution) 

1040 width, height = res.width, res.height 

1041 return ( 

1042 ut.in_interval( 

1043 width, 

1044 limit=Constants.scan_resolution_limit, 

1045 type=tbt.IntervalType.CLOSED, 

1046 ) 

1047 ) and ( 

1048 ut.in_interval( 

1049 height, 

1050 limit=Constants.scan_resolution_limit, 

1051 type=tbt.IntervalType.CLOSED, 

1052 ) 

1053 ) 

1054 

1055 

1056def validate_auto_cb_settings( 

1057 settings: dict, 

1058 yml_format: tbt.YMLFormatVersion, 

1059 step_name: str, 

1060) -> bool: 

1061 """Schema checking for auto contrast/brightness setting dictionary, format specified by yml_format""" 

1062 

1063 if ut.none_value_dictionary(settings): 

1064 return True 

1065 

1066 if settings.get("left") is None or settings.get("top") is None: 

1067 if settings.get("left") is None: 

1068 missing_key = "left" 

1069 else: 

1070 missing_key = "top" 

1071 raise KeyError( 

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

1073 ) 

1074 

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

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

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

1078 

1079 if yml_format.version >= 1.0: 

1080 schema = Schema( 

1081 { 

1082 "left": And( 

1083 float, 

1084 lambda x: ut.in_interval( 

1085 x, 

1086 limit=origin_limit, 

1087 type=tbt.IntervalType.RIGHT_OPEN, 

1088 ), 

1089 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.", 

1090 ), 

1091 "top": And( 

1092 float, 

1093 lambda x: ut.in_interval( 

1094 x, 

1095 limit=origin_limit, 

1096 type=tbt.IntervalType.RIGHT_OPEN, 

1097 ), 

1098 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.", 

1099 ), 

1100 "width": And( 

1101 float, 

1102 lambda x: ut.in_interval( 

1103 x, 

1104 limit=width_limit, 

1105 type=tbt.IntervalType.LEFT_OPEN, 

1106 ), 

1107 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", 

1108 ), 

1109 "height": And( 

1110 float, 

1111 lambda x: ut.in_interval( 

1112 x, 

1113 limit=height_limit, 

1114 type=tbt.IntervalType.LEFT_OPEN, 

1115 ), 

1116 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", 

1117 ), 

1118 }, 

1119 ignore_extra_keys=True, 

1120 ) 

1121 

1122 try: 

1123 schema.validate(settings) 

1124 except UnboundLocalError: 

1125 raise ValueError( 

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

1127 ) 

1128 return True 

1129 

1130 

1131def validate_stage_position( 

1132 microscope: tbt.Microscope, 

1133 step_name: str, 

1134 settings: dict, 

1135 yml_format: tbt.YMLFormatVersion, 

1136) -> bool: 

1137 """Schema checking for stage position dictionary, format specified by yml_format""" 

1138 limits = stage_limits(microscope=microscope) 

1139 

1140 if yml_format.version >= 1.0: 

1141 schema = Schema( 

1142 { 

1143 "x_mm": And( 

1144 float, 

1145 lambda x: ut.in_interval( 

1146 x, 

1147 limit=limits.x_mm, 

1148 type=tbt.IntervalType.CLOSED, 

1149 ), 

1150 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}'", 

1151 ), 

1152 "y_mm": And( 

1153 float, 

1154 lambda x: ut.in_interval( 

1155 x, 

1156 limit=limits.y_mm, 

1157 type=tbt.IntervalType.CLOSED, 

1158 ), 

1159 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}'", 

1160 ), 

1161 "z_mm": And( 

1162 float, 

1163 lambda x: ut.in_interval( 

1164 x, 

1165 limit=limits.z_mm, 

1166 type=tbt.IntervalType.CLOSED, 

1167 ), 

1168 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}'", 

1169 ), 

1170 "r_deg": And( 

1171 float, 

1172 lambda x: ut.in_interval( 

1173 x, 

1174 limit=limits.r_deg, 

1175 type=tbt.IntervalType.RIGHT_OPEN, 

1176 ), 

1177 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}'", 

1178 ), 

1179 "t_deg": And( 

1180 float, 

1181 lambda x: ut.in_interval( 

1182 x, 

1183 limit=limits.t_deg, 

1184 type=tbt.IntervalType.CLOSED, 

1185 ), 

1186 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}'", 

1187 ), 

1188 }, 

1189 ignore_extra_keys=True, 

1190 ) 

1191 

1192 try: 

1193 schema.validate(settings) 

1194 except UnboundLocalError: 

1195 raise ValueError( 

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

1197 ) 

1198 

1199 

1200def validate_beam_settings( 

1201 microscope: tbt.Microscope, 

1202 beam_type: tbt.BeamType, 

1203 settings: dict, 

1204 yml_format: tbt.YMLFormatVersion, 

1205 step_name: str, 

1206) -> bool: 

1207 """Schema checking for beam setting dictionary, format specified by yml_format""" 

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

1209 selected_beam = ut.beam_type(specified_beam, microscope) 

1210 

1211 limits = beam_limits(selected_beam, beam_type) 

1212 

1213 if yml_format.version >= 1.0: 

1214 schema = Schema( 

1215 { 

1216 "voltage_kv": And( 

1217 float, 

1218 lambda x: ut.in_interval( 

1219 x, 

1220 limit=limits.voltage_kv, 

1221 type=tbt.IntervalType.CLOSED, 

1222 ), 

1223 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.", 

1224 ), 

1225 "voltage_tol_kv": And( 

1226 float, 

1227 lambda x: x > 0, 

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

1229 ), 

1230 "current_na": And( 

1231 float, 

1232 lambda x: ut.in_interval( 

1233 x, 

1234 limit=limits.current_na, 

1235 type=tbt.IntervalType.CLOSED, 

1236 ), 

1237 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", 

1238 ), 

1239 "current_tol_na": And( 

1240 float, 

1241 lambda x: x > 0, 

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

1243 ), 

1244 "hfw_mm": And( 

1245 float, 

1246 lambda x: ut.in_interval( 

1247 x, 

1248 limits.hfw_mm, 

1249 type=tbt.IntervalType.CLOSED, 

1250 ), 

1251 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", 

1252 ), 

1253 "working_dist_mm": And( 

1254 float, 

1255 lambda x: ut.in_interval( 

1256 x, 

1257 limit=limits.working_distance_mm, 

1258 type=tbt.IntervalType.CLOSED, 

1259 ), 

1260 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", 

1261 ), 

1262 }, 

1263 ignore_extra_keys=True, 

1264 ) 

1265 

1266 e_beam_schema = Schema( 

1267 { 

1268 "dynamic_focus": Or( 

1269 None, 

1270 bool, 

1271 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.", 

1272 ), 

1273 "tilt_correction": Or( 

1274 None, 

1275 bool, 

1276 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.", 

1277 ), 

1278 }, 

1279 ignore_extra_keys=True, 

1280 ) 

1281 i_beam_schema = Schema( 

1282 { 

1283 "dynamic_focus": Or( 

1284 None, 

1285 False, 

1286 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.", 

1287 ), 

1288 "tilt_correction": Or( 

1289 None, 

1290 False, 

1291 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.", 

1292 ), 

1293 }, 

1294 ignore_extra_keys=True, 

1295 ) 

1296 

1297 try: 

1298 schema.validate(settings) 

1299 if specified_beam.type == tbt.BeamType.ELECTRON: 

1300 e_beam_schema.validate(settings) 

1301 if specified_beam.type == tbt.BeamType.ION: 

1302 i_beam_schema.validate(settings) 

1303 except UnboundLocalError: 

1304 raise ValueError( 

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

1306 ) 

1307 

1308 

1309def validate_detector_settings( 

1310 microscope: tbt.Microscope, 

1311 beam_type: tbt.BeamType, 

1312 settings: dict, 

1313 yml_format: tbt.YMLFormatVersion, 

1314 step_name: str, 

1315) -> bool: 

1316 """Schema checking for detector setting dictionary, format specified by yml_format""" 

1317 

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

1319 devices.device_access(microscope=microscope) 

1320 

1321 # set specified beam to active imaging device 

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

1323 img.set_beam_device( 

1324 microscope=microscope, 

1325 device=specified_beam.device, 

1326 ) 

1327 

1328 auto_cb_db = settings.get("auto_cb") 

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

1330 if use_auto_cb and ( 

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

1332 ): 

1333 raise KeyError( 

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

1335 ) 

1336 

1337 if yml_format.version >= 1.0: 

1338 detector = settings.get("type") 

1339 mode = settings.get("mode") 

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

1341 schema = Schema( 

1342 { 

1343 "brightness": Or( 

1344 None, 

1345 And( 

1346 float, 

1347 lambda x: ut.in_interval( 

1348 x, 

1349 limit=cb_limit, 

1350 type=tbt.IntervalType.LEFT_OPEN, 

1351 ), 

1352 ), 

1353 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}'.", 

1354 ), 

1355 "contrast": Or( 

1356 None, 

1357 And( 

1358 float, 

1359 lambda x: ut.in_interval( 

1360 x, 

1361 limit=cb_limit, 

1362 type=tbt.IntervalType.LEFT_OPEN, 

1363 ), 

1364 ), 

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

1366 ), 

1367 }, 

1368 ignore_extra_keys=True, 

1369 ) 

1370 

1371 # check detector type 

1372 if not ut.valid_enum_entry(detector, tbt.DetectorType): 

1373 raise ValueError( 

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

1375 ) 

1376 detector_type = tbt.DetectorType(detector) 

1377 microscope_detector_types = available_detector_types(microscope=microscope) 

1378 if detector_type.value not in microscope_detector_types: 

1379 raise ValueError( 

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

1381 ) 

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

1383 img.detector_type( 

1384 microscope=microscope, 

1385 detector=detector_type, 

1386 ) 

1387 

1388 # check detector mode 

1389 if not ut.valid_enum_entry(mode, tbt.DetectorMode): 

1390 raise ValueError( 

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

1392 ) 

1393 detector_mode = tbt.DetectorMode(mode) 

1394 microscope_detector_modes = available_detector_modes(microscope=microscope) 

1395 if detector_mode.value not in microscope_detector_modes: 

1396 raise ValueError( 

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

1398 ) 

1399 

1400 # check detector settings 

1401 try: 

1402 schema.validate(settings) 

1403 except UnboundLocalError: 

1404 raise ValueError( 

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

1406 ) 

1407 

1408 

1409def validate_EBSD_EDS_settings( 

1410 yml_format: tbt.YMLFormatVersion, 

1411 connection_host: str, 

1412 connection_port: str, 

1413 ebsd_oem: str, 

1414 eds_oem: str, 

1415) -> bool: 

1416 """Checks EBSD and EDS OEM and connection for supported OEMs""" 

1417 # ensure same manufacturer for both EBSD and EDS 

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

1419 raise NotImplementedError( 

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

1421 ) 

1422 

1423 # Check EBSD OEM 

1424 if not ut.valid_enum_entry(ebsd_oem, tbt.ExternalDeviceOEM): 

1425 raise ValueError( 

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

1427 ) 

1428 ebsd_device = tbt.ExternalDeviceOEM(ebsd_oem) 

1429 # Check EDS OEM 

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

1431 raise ValueError( 

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

1433 ) 

1434 eds_device = tbt.ExternalDeviceOEM(eds_oem) 

1435 

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

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

1438 return True 

1439 

1440 # check EBSD and EDS connection 

1441 try: 

1442 fs_laser.tfs_laser 

1443 except: 

1444 raise SystemError( 

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

1446 ) 

1447 microscope = tbt.Microscope() 

1448 ut.connect_microscope( 

1449 microscope=microscope, 

1450 quiet_output=True, 

1451 connection_host=connection_host, 

1452 connection_port=connection_port, 

1453 ) 

1454 if tbt.ExternalDeviceOEM(eds_oem) != tbt.ExternalDeviceOEM.NONE: 

1455 devices.retract_EDS(microscope=microscope) 

1456 if tbt.ExternalDeviceOEM(ebsd_oem) != tbt.ExternalDeviceOEM.NONE: 

1457 devices.retract_EBSD(microscope=microscope) 

1458 ut.disconnect_microscope( 

1459 microscope=microscope, 

1460 quiet_output=True, 

1461 ) 

1462 return True 

1463 

1464 

1465def validate_general_settings( 

1466 settings: dict, 

1467 yml_format: tbt.YMLFormatVersion, 

1468) -> bool: 

1469 """Schema checking for general setting dictionary, format specified by yml_format. 

1470 Checks microscope connection 

1471 Checks EBSD/EDS connection if valid OEM specified 

1472 """ 

1473 

1474 if settings == {}: 

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

1476 

1477 slice_thickness_limit_um = Constants.slice_thickness_limit_um 

1478 pre_tilt_limit_deg = Constants.pre_tilt_limit_deg_generic 

1479 

1480 if yml_format.version >= 1.0: 

1481 sectioning_axis = settings.get("sectioning_axis") 

1482 connection_host = settings.get("connection_host") 

1483 connection_port = settings.get("connection_port") 

1484 ebsd_oem = settings.get("EBSD_OEM") 

1485 eds_oem = settings.get("EDS_OEM") 

1486 exp_dir = settings.get("exp_dir") 

1487 h5_log_name = settings.get("h5_log_name") 

1488 

1489 # Validate the non-numeric values 

1490 # Check sectioning axis 

1491 if not ut.valid_enum_entry(sectioning_axis, tbt.SectioningAxis): 

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

1493 # TODO 

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

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

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

1497 pre_tilt_limit_deg = Constants.pre_tilt_limit_deg_non_Z_sectioning # overwrite 

1498 warnings.warn( 

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

1500 ) 

1501 # Check connection host and port 

1502 if not ut.valid_microscope_connection( 

1503 host=connection_host, 

1504 port=connection_port, 

1505 ): 

1506 raise ValueError( 

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

1508 ) 

1509 

1510 # check EBSD and EDS 

1511 validate_EBSD_EDS_settings( 

1512 yml_format=yml_format, 

1513 connection_host=connection_host, 

1514 connection_port=connection_port, 

1515 ebsd_oem=ebsd_oem, 

1516 eds_oem=eds_oem, 

1517 ) 

1518 

1519 # Check exp dir 

1520 try: 

1521 Path(exp_dir).mkdir( 

1522 parents=True, 

1523 exist_ok=True, 

1524 ) 

1525 except TypeError: 

1526 raise ValueError( 

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

1528 ) 

1529 # Check h5 log name 

1530 if not isinstance(h5_log_name, str): 

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

1532 

1533 schema = Schema( 

1534 { 

1535 "slice_thickness_um": And( 

1536 float, 

1537 lambda x: ut.in_interval( 

1538 x, 

1539 limit=slice_thickness_limit_um, 

1540 type=tbt.IntervalType.CLOSED, 

1541 ), 

1542 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}'", 

1543 ), 

1544 "max_slice_num": And( 

1545 int, 

1546 lambda x: x > 0, 

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

1548 ), 

1549 "pre_tilt_deg": And( 

1550 float, 

1551 lambda x: ut.in_interval( 

1552 x, 

1553 limit=pre_tilt_limit_deg, 

1554 type=tbt.IntervalType.CLOSED, 

1555 ), 

1556 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}'", 

1557 ), 

1558 "stage_translational_tol_um": And( 

1559 float, 

1560 lambda x: x > 0, 

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

1562 ), 

1563 "stage_angular_tol_deg": And( 

1564 float, 

1565 lambda x: x > 0, 

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

1567 ), 

1568 "step_count": And( 

1569 int, 

1570 lambda x: x > 0, 

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

1572 ), 

1573 }, 

1574 ignore_extra_keys=True, 

1575 ) 

1576 

1577 try: 

1578 schema.validate(settings) 

1579 except UnboundLocalError: 

1580 raise ValueError( 

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

1582 ) 

1583 

1584 

1585def validate_scan_settings( 

1586 microscope: tbt.Microscope, 

1587 beam_type: tbt.BeamType, 

1588 settings: dict, 

1589 yml_format: tbt.YMLFormatVersion, 

1590 step_name: str, 

1591) -> bool: 

1592 """Schema checking for beam setting dictionary, format specified by yml_format""" 

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

1594 selected_beam = ut.beam_type(specified_beam, microscope) 

1595 

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

1597 if yml_format.version >= 1.0: 

1598 limits = scan_limits(selected_beam) 

1599 resolution = settings["resolution"] 

1600 rotation_limit = limits.rotation_deg 

1601 dwell_limit = limits.dwell_us 

1602 

1603 # check resolution 

1604 res = string_to_res(resolution) 

1605 

1606 schema = Schema( 

1607 { 

1608 "resolution": And( 

1609 str, 

1610 lambda x: valid_string_resolution(x), 

1611 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}", 

1612 ), 

1613 "rotation_deg": And( 

1614 float, 

1615 lambda x: ut.in_interval( 

1616 x, 

1617 limit=rotation_limit, 

1618 type=tbt.IntervalType.CLOSED, 

1619 ), 

1620 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'])}.", 

1621 ), 

1622 "dwell_time_us": And( 

1623 float, 

1624 lambda x: ut.in_interval( 

1625 x, 

1626 limit=dwell_limit, 

1627 type=tbt.IntervalType.CLOSED, 

1628 ), 

1629 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'])}.", 

1630 ), 

1631 }, 

1632 ignore_extra_keys=True, 

1633 ) 

1634 

1635 try: 

1636 schema.validate(settings) 

1637 except UnboundLocalError: 

1638 raise ValueError( 

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

1640 ) 

1641 

1642 

1643def stage_position_settings( 

1644 microscope: tbt.Microscope, 

1645 step_name: str, 

1646 general_settings: tbt.GeneralSettings, 

1647 step_stage_settings: dict, 

1648 yml_format: tbt.YMLFormatVersion, 

1649) -> tbt.StageSettings: 

1650 """Create StagePositionUser object from settings, including validation""" 

1651 

1652 if yml_format.version >= 1.0: 

1653 pos_db = step_stage_settings.get("initial_position") 

1654 

1655 rotation_side = step_stage_settings["rotation_side"] 

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

1657 raise NotImplementedError( 

1658 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]}." 

1659 ) 

1660 

1661 validate_stage_position( 

1662 microscope=microscope, 

1663 step_name=step_name, 

1664 settings=pos_db, 

1665 yml_format=yml_format, 

1666 ) 

1667 

1668 initial_position = tbt.StagePositionUser( 

1669 x_mm=pos_db["x_mm"], 

1670 y_mm=pos_db["y_mm"], 

1671 z_mm=pos_db["z_mm"], 

1672 r_deg=pos_db["r_deg"], 

1673 t_deg=pos_db["t_deg"], 

1674 ) 

1675 

1676 pretilt_angle_deg = general_settings.pre_tilt_deg 

1677 sectioning_axis = general_settings.sectioning_axis 

1678 

1679 stage_settings = tbt.StageSettings( 

1680 microscope=microscope, 

1681 initial_position=initial_position, 

1682 pretilt_angle_deg=pretilt_angle_deg, 

1683 sectioning_axis=sectioning_axis, 

1684 rotation_side=tbt.RotationSide(rotation_side), 

1685 # movement_mode=tbt.StageMovementMode.OUT_OF_PLANE, 

1686 ) 

1687 

1688 return stage_settings 

1689 

1690 

1691def validate_pulse_settings( 

1692 settings: dict, 

1693 yml_format: tbt.YMLFormatVersion, 

1694 step_name: str, 

1695) -> bool: 

1696 if yml_format.version >= 1.0: 

1697 wavelength_nm = settings.get("wavelength_nm") 

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

1699 raise NotImplementedError( 

1700 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]}." 

1701 ) 

1702 polarization = settings.get("polarization") 

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

1704 raise NotImplementedError( 

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

1706 ) 

1707 

1708 schema = Schema( 

1709 { 

1710 "divider": And( 

1711 int, 

1712 lambda x: x > 0, 

1713 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.", 

1714 ), 

1715 "energy_uj": And( 

1716 float, 

1717 lambda x: x > 0, 

1718 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.", 

1719 ), 

1720 }, 

1721 ignore_extra_keys=True, 

1722 ) 

1723 try: 

1724 schema.validate(settings) 

1725 except UnboundLocalError: 

1726 raise ValueError( 

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

1728 ) 

1729 

1730 

1731def validate_laser_optics_settings( 

1732 settings: dict, 

1733 yml_format: tbt.YMLFormatVersion, 

1734 step_name: str, 

1735) -> bool: 

1736 if yml_format.version >= 1.0: 

1737 schema = Schema( 

1738 { 

1739 "objective_position_mm": And( 

1740 float, 

1741 lambda x: ut.in_interval( 

1742 x, 

1743 limit=Constants.laser_objective_limit_mm, 

1744 type=tbt.IntervalType.CLOSED, 

1745 ), 

1746 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.", 

1747 ), 

1748 "beam_shift_um_x": And( 

1749 float, 

1750 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.", 

1751 ), 

1752 "beam_shift_um_y": And( 

1753 float, 

1754 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.", 

1755 ), 

1756 }, 

1757 ignore_extra_keys=True, 

1758 ) 

1759 try: 

1760 schema.validate(settings) 

1761 except UnboundLocalError: 

1762 raise ValueError( 

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

1764 ) 

1765 

1766 

1767def validate_laser_box_settings( 

1768 settings: dict, 

1769 yml_format: tbt.YMLFormatVersion, 

1770 step_name: str, 

1771) -> bool: 

1772 if yml_format.version >= 1.0: 

1773 # scan type 

1774 scan_type = settings.get("scan_type") 

1775 scan_types = [ 

1776 tbt.LaserScanType.RASTER, 

1777 tbt.LaserScanType.SERPENTINE, 

1778 ] 

1779 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]}." 

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

1781 raise NotImplementedError(scan_type_error_msg) 

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

1783 raise NotImplementedError(scan_type_error_msg) 

1784 

1785 # coordinate reference 

1786 coordinate_ref = settings.get("coordinate_ref") 

1787 coord_refs = [ 

1788 tbt.CoordinateReference.CENTER, 

1789 tbt.CoordinateReference.UPPER_CENTER, 

1790 tbt.CoordinateReference.UPPER_LEFT, 

1791 ] 

1792 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]}." 

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

1794 raise NotImplementedError(coord_error_msg) 

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

1796 raise NotImplementedError(coord_error_msg) 

1797 

1798 schema = Schema( 

1799 { 

1800 "passes": And( 

1801 int, 

1802 lambda x: x > 0, 

1803 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.", 

1804 ), 

1805 "size_x_um": And( 

1806 float, 

1807 lambda x: x > 0, 

1808 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.", 

1809 ), 

1810 "size_y_um": And( 

1811 float, 

1812 lambda x: x > 0, 

1813 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.", 

1814 ), 

1815 "pitch_x_um": And( 

1816 float, 

1817 lambda x: x > 0, 

1818 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.", 

1819 ), 

1820 "pitch_y_um": And( 

1821 float, 

1822 lambda x: x > 0, 

1823 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.", 

1824 ), 

1825 }, 

1826 ignore_extra_keys=True, 

1827 ) 

1828 try: 

1829 schema.validate(settings) 

1830 except UnboundLocalError: 

1831 raise ValueError( 

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

1833 ) 

1834 

1835 

1836def validate_laser_line_settings( 

1837 settings: dict, 

1838 yml_format: tbt.YMLFormatVersion, 

1839 step_name: str, 

1840) -> bool: 

1841 if yml_format.version >= 1.0: 

1842 # scan type 

1843 scan_type = settings.get("scan_type") 

1844 scan_types = [ 

1845 tbt.LaserScanType.SINGLE, 

1846 tbt.LaserScanType.LAP, 

1847 ] 

1848 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]}." 

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

1850 raise NotImplementedError(scan_type_error_msg) 

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

1852 raise NotImplementedError(scan_type_error_msg) 

1853 

1854 schema = Schema( 

1855 { 

1856 "passes": And( 

1857 int, 

1858 lambda x: x > 0, 

1859 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.", 

1860 ), 

1861 "size_um": And( 

1862 float, 

1863 lambda x: x > 0, 

1864 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.", 

1865 ), 

1866 "pitch_um": And( 

1867 float, 

1868 lambda x: x > 0, 

1869 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.", 

1870 ), 

1871 }, 

1872 ignore_extra_keys=True, 

1873 ) 

1874 try: 

1875 schema.validate(settings) 

1876 except UnboundLocalError: 

1877 raise ValueError( 

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

1879 ) 

1880 

1881 

1882def validate_laser_mode_settings( 

1883 settings: dict, 

1884 yml_format: tbt.YMLFormatVersion, 

1885 step_name: str, 

1886) -> bool: 

1887 if yml_format.version >= 1.0: 

1888 mode = settings.get("mode") 

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

1890 raise NotImplementedError( 

1891 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]}." 

1892 ) 

1893 

1894 schema = Schema( 

1895 { 

1896 "rotation_deg": And( 

1897 float, 

1898 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.", 

1899 ), 

1900 }, 

1901 ignore_extra_keys=True, 

1902 ) 

1903 schema_fine = Schema( 

1904 { 

1905 "pixel_dwell_ms": Or( 

1906 None, 

1907 "null", 

1908 "None", 

1909 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.", 

1910 ), 

1911 "pulses_per_pixel": And( 

1912 int, 

1913 lambda x: x > 0, 

1914 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.", 

1915 ), 

1916 }, 

1917 ignore_extra_keys=True, 

1918 ) 

1919 schema_coarse = Schema( 

1920 { 

1921 "pixel_dwell_ms": And( 

1922 float, 

1923 lambda x: x > 0.0, 

1924 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.", 

1925 ), 

1926 "pulses_per_pixel": Or( 

1927 None, 

1928 "null", 

1929 "None", 

1930 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.", 

1931 ), 

1932 }, 

1933 ignore_extra_keys=True, 

1934 ) 

1935 try: 

1936 schema.validate(settings) 

1937 if mode == "fine": 

1938 schema_fine.validate(settings) 

1939 if mode == "coarse": 

1940 schema_coarse.validate(settings) 

1941 except UnboundLocalError: 

1942 raise ValueError( 

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

1944 ) 

1945 

1946 

1947def validate_laser_pattern_settings( 

1948 settings: dict, 

1949 yml_format: tbt.YMLFormatVersion, 

1950 step_name: str, 

1951) -> tbt.LaserPatternType: 

1952 if yml_format.version >= 1.0: 

1953 # determine type of pattern (only one allowed) 

1954 type_set_db = settings.get("type") 

1955 if type_set_db is None: 

1956 raise KeyError( 

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

1958 ) 

1959 

1960 box_settings = type_set_db.get("box") 

1961 line_settings = type_set_db.get("line") 

1962 if box_settings is None: 

1963 box_pattern = False 

1964 else: 

1965 box_pattern = not ut.none_value_dictionary(box_settings) 

1966 if line_settings is None: 

1967 line_pattern = False 

1968 else: 

1969 line_pattern = not ut.none_value_dictionary(line_settings) 

1970 if box_pattern == line_pattern == False: 

1971 raise KeyError( 

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

1973 ) 

1974 if box_pattern == line_pattern == True: 

1975 raise KeyError( 

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

1977 ) 

1978 validate_laser_mode_settings( 

1979 settings=settings, 

1980 yml_format=yml_format, 

1981 step_name=step_name, 

1982 ) 

1983 

1984 if box_pattern: 

1985 validate_laser_box_settings( 

1986 settings=box_settings, 

1987 yml_format=yml_format, 

1988 step_name=step_name, 

1989 ) 

1990 return tbt.LaserPatternType.BOX 

1991 elif line_pattern: 

1992 validate_laser_line_settings( 

1993 settings=line_settings, 

1994 yml_format=yml_format, 

1995 step_name=step_name, 

1996 ) 

1997 return tbt.LaserPatternType.LINE 

1998 else: 

1999 raise ValueError( 

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

2001 ) 

2002 

2003 

2004def validate_fib_pattern_settings( 

2005 microscope: tbt.Microscope, 

2006 settings: dict, 

2007 yml_format: tbt.YMLFormatVersion, 

2008 step_name: str, 

2009) -> Union[ 

2010 tbt.FIBRectanglePattern, 

2011 tbt.FIBRegularCrossSection, 

2012 tbt.FIBCleaningCrossSection, 

2013 tbt.FIBStreamPattern, 

2014]: 

2015 """Schema checking for fib pattern setting dictionary, format specified by yml_format""" 

2016 

2017 if yml_format.version >= 1.0: 

2018 application_file = settings.get("application_file") 

2019 # determine type of pattern (only one allowed) 

2020 type_set_db = settings.get("type") 

2021 if type_set_db is None: 

2022 raise KeyError( 

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

2024 ) 

2025 rectangle_settings = type_set_db.get("rectangle") 

2026 regular_cross_section_settings = type_set_db.get("regular_cross_section") 

2027 cleaning_cross_section_settings = type_set_db.get("cleaning_cross_section") 

2028 selected_area_settings = type_set_db.get("selected_area") 

2029 

2030 pattern_settings = [ 

2031 rectangle_settings, 

2032 regular_cross_section_settings, 

2033 cleaning_cross_section_settings, 

2034 selected_area_settings, 

2035 ] 

2036 ( 

2037 rectangle_pattern, 

2038 regular_cross_section_pattern, 

2039 cleaning_cross_section_pattern, 

2040 selected_area_pattern, 

2041 ) = (None, None, None, None) 

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

2043 # assert pattern_names == [ 

2044 # "rectangle", 

2045 # "regular_cross_section", 

2046 # "cleaning_cross_section", 

2047 # "selected_area", 

2048 # ] 

2049 pattern_names = [ 

2050 "rectangle", 

2051 "regular_cross_section", 

2052 "cleaning_cross_section", 

2053 "selected_area", 

2054 ] 

2055 pattern_types = [ 

2056 rectangle_pattern, 

2057 regular_cross_section_pattern, 

2058 cleaning_cross_section_pattern, 

2059 selected_area_pattern, 

2060 ] 

2061 

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

2063 db = pattern_settings[type] 

2064 if db is None: 

2065 pattern_types[type] = False 

2066 else: 

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

2068 

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

2070 if sum(pattern_types) != 1: 

2071 raise KeyError( 

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

2073 ) 

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

2075 

2076 # check application file 

2077 valid_applications = application_files(microscope=microscope) 

2078 # TODO make this print out pretty 

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

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

2081 if application_file not in valid_applications: 

2082 raise ValueError( 

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

2084 ) 

2085 

2086 if pattern_type == tbt.FIBPatternType.RECTANGLE: 

2087 validate_fib_box_settings( 

2088 settings=rectangle_settings, 

2089 yml_format=yml_format, 

2090 step_name=step_name, 

2091 pattern_type=pattern_type, 

2092 ) 

2093 

2094 pattern = tbt.FIBPattern( 

2095 application=application_file, 

2096 type=pattern_type, 

2097 geometry=tbt.FIBRectanglePattern( 

2098 center_um=tbt.Point( 

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

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

2101 ), 

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

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

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

2105 scan_direction=tbt.FIBPatternScanDirection( 

2106 rectangle_settings.get("scan_direction") 

2107 ), 

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

2109 ), 

2110 ) 

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

2112 try: 

2113 geometry = pattern.geometry 

2114 microscope.patterning.set_default_application_file(pattern.application) 

2115 microscope.patterning.create_rectangle( 

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

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

2118 width=geometry.width_um * Conversions.UM_TO_M, 

2119 height=geometry.height_um * Conversions.UM_TO_M, 

2120 depth=geometry.depth_um * Conversions.UM_TO_M, 

2121 ) 

2122 microscope.patterning.clear_patterns() 

2123 except: 

2124 raise ValueError( 

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

2126 ) 

2127 

2128 return pattern 

2129 elif pattern_type == tbt.FIBPatternType.REGULAR_CROSS_SECTION: 

2130 validate_fib_box_settings( 

2131 settings=regular_cross_section_settings, 

2132 yml_format=yml_format, 

2133 step_name=step_name, 

2134 pattern_type=pattern_type, 

2135 ) 

2136 pattern = tbt.FIBPattern( 

2137 application=application_file, 

2138 type=pattern_type, 

2139 geometry=tbt.FIBRegularCrossSection( 

2140 center_um=tbt.Point( 

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

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

2143 ), 

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

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

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

2147 scan_direction=tbt.FIBPatternScanDirection( 

2148 regular_cross_section_settings.get("scan_direction") 

2149 ), 

2150 scan_type=tbt.FIBPatternScanType( 

2151 regular_cross_section_settings.get("scan_type") 

2152 ), 

2153 ), 

2154 ) 

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

2156 try: 

2157 geometry = pattern.geometry 

2158 microscope.patterning.set_default_application_file(pattern.application) 

2159 microscope.patterning.create_regular_cross_section( 

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

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

2162 width=geometry.width_um * Conversions.UM_TO_M, 

2163 height=geometry.height_um * Conversions.UM_TO_M, 

2164 depth=geometry.depth_um * Conversions.UM_TO_M, 

2165 ) 

2166 microscope.patterning.clear_patterns() 

2167 except: 

2168 raise ValueError( 

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

2170 ) 

2171 return pattern 

2172 elif pattern_type == tbt.FIBPatternType.CLEANING_CROSS_SECTION: 

2173 validate_fib_box_settings( 

2174 settings=cleaning_cross_section_settings, 

2175 yml_format=yml_format, 

2176 step_name=step_name, 

2177 pattern_type=pattern_type, 

2178 ) 

2179 pattern = tbt.FIBPattern( 

2180 application=application_file, 

2181 type=pattern_type, 

2182 geometry=tbt.FIBCleaningCrossSection( 

2183 center_um=tbt.Point( 

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

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

2186 ), 

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

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

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

2190 scan_direction=tbt.FIBPatternScanDirection( 

2191 cleaning_cross_section_settings.get("scan_direction") 

2192 ), 

2193 scan_type=tbt.FIBPatternScanType( 

2194 cleaning_cross_section_settings.get("scan_type") 

2195 ), 

2196 ), 

2197 ) 

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

2199 try: 

2200 geometry = pattern.geometry 

2201 microscope.patterning.set_default_application_file(pattern.application) 

2202 microscope.patterning.create_cleaning_cross_section( 

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

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

2205 width=geometry.width_um * Conversions.UM_TO_M, 

2206 height=geometry.height_um * Conversions.UM_TO_M, 

2207 depth=geometry.depth_um * Conversions.UM_TO_M, 

2208 ) 

2209 microscope.patterning.clear_patterns() 

2210 except: 

2211 raise ValueError( 

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

2213 ) 

2214 return pattern 

2215 elif pattern_type == tbt.FIBPatternType.SELECTED_AREA: 

2216 validate_fib_selected_area_settings( 

2217 settings=selected_area_settings, 

2218 yml_format=yml_format, 

2219 step_name=step_name, 

2220 pattern_type=pattern_type, 

2221 ) 

2222 pattern = tbt.FIBPattern( 

2223 application=application_file, 

2224 type=pattern_type, 

2225 geometry=tbt.FIBStreamPattern( 

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

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

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

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

2230 ), 

2231 ) 

2232 

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

2234 try: 

2235 microscope.patterning.set_default_application_file(pattern.application) 

2236 microscope.patterning.create_rectangle( 

2237 center_x=0.0, 

2238 center_y=0.0, 

2239 width=10.0e-6, 

2240 height=10.0e-6, 

2241 depth=1.0e-6, 

2242 ) 

2243 microscope.patterning.clear_patterns() 

2244 except: 

2245 raise ValueError( 

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

2247 ) 

2248 

2249 return pattern 

2250 else: 

2251 raise KeyError( 

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

2253 ) 

2254 

2255 

2256def validate_fib_box_settings( 

2257 settings: dict, 

2258 yml_format: tbt.YMLFormatVersion, 

2259 step_name: str, 

2260 pattern_type: tbt.FIBPatternType, 

2261) -> bool: 

2262 if yml_format.version >= 1.0: 

2263 # flattens nested dictionary, adding "_" separator 

2264 flat_settings = ut._flatten(settings) 

2265 schema = Schema( 

2266 { 

2267 "center_x_um": And( 

2268 float, 

2269 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.", 

2270 ), 

2271 "center_y_um": And( 

2272 float, 

2273 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.", 

2274 ), 

2275 "width_um": And( 

2276 float, 

2277 lambda x: x > 0, 

2278 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.", 

2279 ), 

2280 "height_um": And( 

2281 float, 

2282 lambda x: x > 0, 

2283 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.", 

2284 ), 

2285 "depth_um": And( 

2286 float, 

2287 lambda x: x > 0, 

2288 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.", 

2289 ), 

2290 "scan_direction": And( 

2291 str, 

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

2293 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]}", 

2294 ), 

2295 "scan_type": And( 

2296 str, 

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

2298 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]}", 

2299 ), 

2300 }, 

2301 ignore_extra_keys=True, 

2302 ) 

2303 try: 

2304 schema.validate(flat_settings) 

2305 except UnboundLocalError: 

2306 raise ValueError( 

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

2308 ) 

2309 return True 

2310 

2311 

2312def validate_fib_selected_area_settings( 

2313 settings: dict, 

2314 yml_format: tbt.YMLFormatVersion, 

2315 step_name: str, 

2316 pattern_type: tbt.FIBPatternType, 

2317) -> bool: 

2318 if yml_format.version >= 1.0: 

2319 schema = Schema( 

2320 { 

2321 "dwell_us": And( 

2322 float, 

2323 lambda x: x > 0, 

2324 # check all float error versions of modulus 

2325 Or( 

2326 lambda x: math.isclose( 

2327 x % Constants.stream_pattern_base_dwell_us, 

2328 0, 

2329 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5, 

2330 ), 

2331 lambda x: math.isclose( 

2332 x % Constants.stream_pattern_base_dwell_us, 

2333 Constants.stream_pattern_base_dwell_us, 

2334 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5, 

2335 ), 

2336 lambda x: math.isclose( 

2337 x % Constants.stream_pattern_base_dwell_us, 

2338 -Constants.stream_pattern_base_dwell_us, 

2339 abs_tol=Constants.stream_pattern_base_dwell_us / 1e5, 

2340 ), 

2341 ), 

2342 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.", 

2343 ), 

2344 "repeats": And( 

2345 int, 

2346 lambda x: x > 0, 

2347 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.", 

2348 ), 

2349 "recipe_file": And( 

2350 str, 

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

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

2353 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.", 

2354 ), 

2355 "mask_file": And( 

2356 str, 

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

2358 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.", 

2359 ), 

2360 }, 

2361 ignore_extra_keys=True, 

2362 ) 

2363 try: 

2364 schema.validate(settings) 

2365 except UnboundLocalError: 

2366 raise ValueError( 

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

2368 ) 

2369 return True 

2370 

2371 

2372def step( 

2373 microscope: tbt.Microscope, 

2374 # slice_number: str, 

2375 step_name: str, 

2376 step_settings: dict, 

2377 general_settings: tbt.GeneralSettings, 

2378 yml_format: tbt.YMLFormatVersion, 

2379) -> tbt.Step: 

2380 """Create step object for different step types, including validation.""" 

2381 

2382 # parsing settings 

2383 step_type_value = step_settings[yml_format.step_general_key][ 

2384 yml_format.step_type_key 

2385 ] 

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

2387 raise NotImplementedError( 

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

2389 ) 

2390 step_type = tbt.StepType(step_type_value) 

2391 

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

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

2394 raise KeyError( 

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

2396 ) 

2397 step_frequency = step_settings[yml_format.step_general_key][ 

2398 yml_format.step_frequency_key 

2399 ] 

2400 if not isinstance(step_frequency, int) or (step_frequency < 1): 

2401 raise KeyError( 

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

2403 ) 

2404 stage_db = step_settings[yml_format.step_general_key][ 

2405 yml_format.step_stage_settings_key 

2406 ] 

2407 

2408 # check and validate stage 

2409 stage_settings = stage_position_settings( 

2410 microscope=microscope, 

2411 step_name=step_name, 

2412 general_settings=general_settings, 

2413 step_stage_settings=stage_db, 

2414 yml_format=yml_format, 

2415 ) 

2416 

2417 # TODO 

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

2419 if step_type == tbt.StepType.EBSD: 

2420 operation_settings = ebsd( 

2421 microscope=microscope, 

2422 step_settings=step_settings, 

2423 step_name=step_name, 

2424 yml_format=yml_format, 

2425 ) 

2426 if step_type == tbt.StepType.EDS: 

2427 operation_settings = eds( 

2428 microscope=microscope, 

2429 step_settings=step_settings, 

2430 step_name=step_name, 

2431 yml_format=yml_format, 

2432 ) 

2433 if step_type == tbt.StepType.IMAGE: 

2434 operation_settings = image( 

2435 microscope=microscope, 

2436 step_settings=step_settings, 

2437 step_name=step_name, 

2438 yml_format=yml_format, 

2439 ) 

2440 if step_type == tbt.StepType.LASER: 

2441 operation_settings = laser( 

2442 microscope=microscope, 

2443 step_settings=step_settings, 

2444 step_name=step_name, 

2445 yml_format=yml_format, 

2446 ) 

2447 if step_type == tbt.StepType.CUSTOM: 

2448 operation_settings = custom( 

2449 microscope=microscope, 

2450 step_settings=step_settings, 

2451 step_name=step_name, 

2452 yml_format=yml_format, 

2453 ) 

2454 if step_type == tbt.StepType.FIB: 

2455 operation_settings = fib( 

2456 microscope=microscope, 

2457 step_settings=step_settings, 

2458 step_name=step_name, 

2459 yml_format=yml_format, 

2460 ) 

2461 

2462 step_object = tbt.Step( 

2463 type=step_type, 

2464 name=step_name, 

2465 number=step_number, 

2466 frequency=step_frequency, 

2467 stage=stage_settings, 

2468 operation_settings=operation_settings, 

2469 ) 

2470 

2471 return step_object