Coverage for src/sdynpy/fileio/sdynpy_psv.py: 55%

3674 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-18 21:32 +0000

1# -*- coding: utf-8 -*- 

2""" 

3Load and wrap data from Polytec PSV files. 

4 

5This module provides a Python wrapper around the Polytec ``Polyfile`` COM 

6interface, along with convenience methods for extracting common data products 

7into ``sdynpy`` objects. 

8 

9Notes 

10----- 

11- This module requires ``pywin32`` and therefore only works on Windows. 

12- Many objects exposed by this module are thin wrappers around COM objects. 

13- Where the exact runtime type returned by COM is uncertain, conservative type 

14 annotations such as ``Any`` are used. 

15""" 

16 

17from __future__ import annotations 

18 

19""" 

20Copyright 2022 National Technology & Engineering Solutions of Sandia, 

21LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. 

22Government retains certain rights in this software. 

23 

24This program is free software: you can redistribute it and/or modify 

25it under the terms of the GNU General Public License as published by 

26the Free Software Foundation, either version 3 of the License, or 

27(at your option) any later version. 

28 

29This program is distributed in the hope that it will be useful, 

30but WITHOUT ANY WARRANTY; without even the implied warranty of 

31MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

32GNU General Public License for more details. 

33 

34You should have received a copy of the GNU General Public License 

35along with this program. If not, see <https://www.gnu.org/licenses/>. 

36""" 

37 

38 

39from enum import Enum, IntFlag 

40from io import BytesIO 

41from os import PathLike 

42from typing import Any, TypeVar 

43from collections import defaultdict 

44 

45import numpy as np 

46from PIL import Image, ImageDraw 

47import matplotlib.pyplot as plt 

48 

49try: 

50 import win32com.client as client 

51 from pywintypes import com_error 

52except ModuleNotFoundError: 

53 client = None 

54 # Fallback keeps ``except com_error`` clauses valid even when pywin32 

55 # is unavailable. Those paths are normally unreachable because PolyFile 

56 # raises during initialization when ``client is None``. 

57 com_error = RuntimeError 

58 

59from sdynpy.core.sdynpy_coordinate import coordinate_array 

60from sdynpy.core.sdynpy_data import ( 

61 coherence_array, 

62 multiple_coherence_array, 

63 power_spectral_density_array, 

64 time_history_array, 

65 transfer_function_array, 

66 CoherenceArray, 

67 MultipleCoherenceArray, 

68 PowerSpectralDensityArray, 

69 TimeHistoryArray, 

70 TransferFunctionArray, 

71) 

72from sdynpy.core.sdynpy_geometry import ( 

73 Geometry, 

74 coordinate_system_array, 

75 node_array, 

76) 

77from sdynpy.signal_processing.sdynpy_geometry_fitting import intersection_point_multiple_lines, distance_point_line 

78 

79VERBOSE = False 

80 

81_FlagT = TypeVar("_FlagT", bound=IntFlag) 

82 

83 

84class AcquisitionMode(Enum): 

85 """Measurement acquisition mode.""" 

86 

87 TIME = 0 

88 FFT = 1 

89 ZOOM_FFT = 2 

90 MULTI_FRAME = 3 

91 FAST_SCAN = 4 

92 MODE_ORDER_TRACKING = 5 

93 IQ = 6 

94 IN_PLANE = 7 

95 

96 

97class AverageType(Enum): 

98 """Averaging mode.""" 

99 

100 OFF = 0 

101 MAGNITUDE = 1 

102 COMPLEX = 2 

103 TIME = 3 

104 PEAK_HOLD = 4 

105 

106 

107class DigitalFilterQuality(Enum): 

108 """Quality setting for digital filters.""" 

109 

110 VERY_LOW = 0 

111 LOW = 1 

112 MIDDLE = 2 

113 HIGH = 3 

114 VERY_HIGH = 4 

115 VERY_HIGH_200 = 5 

116 VERY_HIGH_300 = 6 

117 VERY_HIGH_400 = 7 

118 VERY_HIGH_500 = 8 

119 

120 

121class DigitalFilterType(Enum): 

122 """Digital filter type.""" 

123 

124 LOWPASS = 0 

125 HIGHPASS = 1 

126 BANDPASS = 2 

127 NOTCH = 3 

128 NONE = 4 

129 

130 

131class Direction(Enum): 

132 """Measurement direction.""" 

133 

134 Z_POS = 0 

135 Z_NEG = 1 

136 X_POS = 2 

137 X_NEG = 3 

138 Y_POS = 4 

139 Y_NEG = 5 

140 VECTOR_3D = 6 

141 

142 

143class Coupling(Enum): 

144 """Input coupling mode.""" 

145 

146 AC = 0 

147 DC = 1 

148 UNKNOWN = 2 

149 

150 

151class PhysicalQuantity(Enum): 

152 """Physical quantity identifier.""" 

153 

154 ACCELERATION = 0 

155 ANGLE = 1 

156 ANGULAR_ACCELERATION = 2 

157 ANGULAR_VELOCITY = 3 

158 DISPLACEMENT = 4 

159 ELECTRICAL_CURRENT = 5 

160 FORCE = 6 

161 POWER = 7 

162 PRESSURE = 8 

163 REVOLUTION = 9 

164 RPM = 10 

165 SOUND_PRESSURE = 11 

166 TORQUE = 12 

167 VELOCITY = 13 

168 VOLTAGE = 14 

169 COUNTS = 15 

170 VOLUME = 16 

171 VOLUME_VELOCITY = 17 

172 VOLUME_ACCELERATION = 18 

173 STRAIN = 19 

174 GRAVITY = 20 

175 STRESS = 21 

176 TIME = 22 

177 FREQUENCY = 23 

178 ANGLE_DEGREE = 24 

179 ANGULAR_ACCELERATION_DEGREE = 25 

180 ANGULAR_VELOCITY_DEGREE = 26 

181 

182 

183class ChannelType(Enum): 

184 """Acquisition channel type.""" 

185 

186 ANALOG = 0 

187 ANALOG_FIXED = 1 

188 FRINGE_COUNTER = 2 

189 LOGICAL = 3 

190 MODULATED = 4 

191 DIGITAL_VIB = 5 

192 MULTI_POINT_VIB = 6 

193 LOOP_BACK = 7 

194 ANALOG_HF = 8 

195 

196 

197class WindowFunction(Enum): 

198 """Window function used for time/FFT processing.""" 

199 

200 RECTANGLE = 0 

201 BARTLETT = 1 

202 BLACKMANHARRIS = 2 

203 EXPONENTIAL = 3 

204 FLATTOP = 4 

205 FORCE = 5 

206 HAMMING = 6 

207 HANNING = 7 

208 TAPEREDHANNING = 8 

209 

210 

211class BandwidthExtension(Enum): 

212 """Bandwidth-extension setting.""" 

213 

214 NONE = 0 

215 OFF = 1 

216 ON1 = 2 

217 ON2 = 3 

218 

219 

220class WaveformType(Enum): 

221 """Signal-generator waveform type.""" 

222 

223 SINE = 0 

224 SQUARE = 1 

225 TRIANGLE = 2 

226 RAMP = 3 

227 SWEEP = 4 

228 BURST_CHIRP = 5 

229 BURST_RANDOM = 6 

230 PERIODIC_CHIRP = 7 

231 PSEUDORANDOM = 8 

232 WHITE_NOISE = 9 

233 USER_DEFINED = 10 

234 PULSE = 11 

235 MULTI_CARRIER_CW = 12 

236 

237 

238class MultiFrameMode(Enum): 

239 """Multi-frame acquisition mode.""" 

240 

241 AUTOMATIC = 0 

242 REGULAR = 1 

243 MANUAL = 2 

244 

245 

246class SignalEnhancementMode(Enum): 

247 """Signal enhancement (SE) mode.""" 

248 

249 FAST = 0 

250 FAST_STANDARD = 1 

251 STANDARD = 2 

252 STANDARD_BEST = 3 

253 BEST = 4 

254 

255 

256class TriggerEdge(Enum): 

257 """Trigger edge polarity.""" 

258 

259 RISING = 0 

260 FALLING = 1 

261 

262 

263class TriggerSource(Enum): 

264 """Trigger source.""" 

265 

266 OFF = 0 

267 EXTERNAL = 1 

268 ANALOG = 2 

269 INTERNAL = 3 

270 

271 

272class ControllerCaps(IntFlag): 

273 """Controller capability flags.""" 

274 

275 NONE = 0 

276 POWER_UP = 1 

277 SENSOR_HEAD = 2 

278 SERVICE_MODE = 4 

279 DIGITAL_DEMODULATION = 8 

280 MULTIPLE_OVERRANGE = 16 

281 SIGNAL_ENHANCEMENT = 32 

282 MEASUREMENT_MODE = 64 

283 MULTIPLE_SENSOR_HEADS = 128 

284 OVERRANGE = 256 

285 PSV500_BEHAVIOUR = 512 

286 NO_DECODER = 1024 

287 DECODER_CLEAR_MODE = 2048 

288 IS_PHYSICAL_UNIT = 4096 

289 WRITE_LOCK = 8192 

290 RECORDER = 16384 

291 WEB_SERVER = 32768 

292 

293 

294def decode_controller_caps(caps_value: int) -> ControllerCaps: 

295 """Decode an integer bitmask into :class:`ControllerCaps`.""" 

296 return ControllerCaps(int(caps_value)) 

297 

298 

299def controller_caps_list(caps_value: int) -> list[ControllerCaps]: 

300 """Return individual controller capability flags set in ``caps_value``.""" 

301 caps = decode_controller_caps(caps_value) 

302 return [flag for flag in ControllerCaps if flag is not ControllerCaps.NONE and (caps & flag)] 

303 

304 

305class SensorHeadCaps(IntFlag): 

306 """Sensor-head capability flags.""" 

307 

308 NONE = 0 

309 FOCUS_RELATIVE = 1 

310 FOCUS_ABSOLUTE = 2 

311 AUTO_FOCUS = 4 

312 MANUAL_FOCUS_ENABLE = 8 

313 SHUTTER = 16 

314 DIMMER = 32 

315 SIGNAL_LEVEL = 64 

316 VIDEO_CAMERA = 128 

317 SIGNAL_BALANCE = 256 

318 FOCUS_LIMIT = 512 

319 FOCUS_PROGRESS = 1024 

320 BRAGG_CELL_FREQUENCY = 2048 

321 ILLUMINATION_STATE = 4096 

322 ILLUMINATION_INTENSITY = 8192 

323 AUTO_FOCUS_IN_RANGE = 16384 

324 AUTO_FOCUS_IN_AREA = 32768 

325 LASER_DELAY = 65536 

326 SIGNAL_LEVEL_B = 131072 

327 BEAM_GAP = 262144 

328 STANDOFF_DIST = 524288 

329 SENSITIVITY = 1048576 

330 RETICLE = 2097152 

331 OVER_TEMP = 4194304 

332 TEMPERATURE = 8388608 

333 COHERENCE_OPTIMIZER = 16777216 

334 ALIGNMENT_LASER_SHUTTER = 33554432 

335 OBJECTIVE_TEMPERATURE = 67108864 

336 FOCUS_STEP_DEVIATION = 134217728 

337 LASER_WAVELENGTH = 268435456 

338 LASER_WAVELENGTH_STABILIZATION = -2147483648 

339 PILOT_LASER_SHUTTER = 536870912 

340 PILOT_LASER_DIMMER = 1073741824 

341 

342 

343class SensorHeadCaps2(IntFlag): 

344 """Extended sensor-head capability flags.""" 

345 

346 NONE = 0 

347 LASER_TEMPERATURE = 1 

348 LASER_TEMPERATURE_OUT_OF_RANGE = 2 

349 SONDE = 4 

350 UNIQUE_ID = 8 

351 RSSI_LEVEL_BLINK = 16 

352 RGB_LED_COLOR = 32 

353 LASER_POWER = 64 

354 DIFFERENTIAL_MEASUREMENT_MODE = 128 

355 EXTERNAL_SCANNER_CONTROL = 256 

356 LASER_POWER_SUPPLY = 512 

357 Q_TEC = 1024 

358 

359 

360class SignalBalanceState(Enum): 

361 """Signal-balance state.""" 

362 

363 SEEK = 0 

364 MINUS3 = 1 

365 MINUS2 = 2 

366 MINUS1 = 3 

367 BALANCED = 4 

368 PLUS1 = 5 

369 PLUS2 = 6 

370 PLUS3 = 7 

371 

372 

373class FilterType(Enum): 

374 """Filter type used in controller/filter settings.""" 

375 

376 LOW_PASS = 0 

377 HIGH_PASS = 1 

378 ADAPTIVE = 2 

379 TRACKING = 3 

380 DC_RESPONSE = 4 

381 TRANSLATION = 5 

382 ROTATION = 6 

383 RPM_RESPONSE = 7 

384 ASE = 8 

385 DISPLACEMENT_LOW_PASS = 9 

386 DISPLACEMENT_HIGH_PASS = 10 

387 ACCELERATION_LOW_PASS = 11 

388 ACCELERATION_HIGH_PASS = 12 

389 DISPLACEMENT_ASE = 13 

390 ACCELERATION_ASE = 14 

391 

392 

393class QuantityType(Enum): 

394 """Quantity type used in controller quantity settings.""" 

395 

396 ACCELERATION = 0 

397 ANGLE = 1 

398 ANGULAR_ACCELERATION = 2 

399 ANGULAR_VELOCITY = 3 

400 CHARGE = 4 

401 COUNTS = 5 

402 CURRENT = 6 

403 DISPLACEMENT = 7 

404 FORCE = 8 

405 FREQUENCY = 9 

406 LUMINOUS_INTENSITY = 10 

407 MASS = 11 

408 ORDER = 12 

409 POWER = 13 

410 PRESSURE = 14 

411 RESISTANCE = 15 

412 REVOLUTION = 16 

413 RPM = 17 

414 SOUND_PRESSURE = 18 

415 SUBSTANCE = 19 

416 TEMPERATURE = 20 

417 TIME = 21 

418 TORQUE = 22 

419 USER = 23 

420 VELOCITY = 24 

421 VOLTAGE = 25 

422 WORK = 26 

423 VOLUME = 27 

424 VOLUME_VELOCITY = 28 

425 VOLUME_ACCELERATION = 29 

426 STRAIN = 30 

427 GRAVITATIONAL_ACCELERATION = 31 

428 STRESS = 32 

429 ANGLE_DEGREE = 33 

430 ANGULAR_ACCELERATION_DEGREE = 34 

431 ANGULAR_VELOCITY_DEGREE = 35 

432 

433 

434class AlignmentPoint3DCaps(IntFlag): 

435 """3D alignment-point capability flags.""" 

436 

437 COORD_3D = 1 

438 DISTANCE = 2 

439 QUALITY = 4 

440 SCANNER = 8 

441 VIDEO = 16 

442 

443 

444class AlignmentPoint3DType(Enum): 

445 """3D alignment-point type.""" 

446 

447 ALIGN = 0 

448 OBJ_FREE = 1 

449 OBJ_ORIGIN = 2 

450 OBJ_POS_X_AXIS = 3 

451 OBJ_POS_Y_AXIS = 4 

452 OBJ_POS_Z_AXIS = 5 

453 OBJ_X_POS_Y_PLANE = 6 

454 OBJ_X_POS_Z_PLANE = 7 

455 OBJ_Y_POS_X_PLANE = 8 

456 OBJ_Y_POS_Z_PLANE = 9 

457 OBJ_Z_POS_X_PLANE = 10 

458 OBJ_Z_POS_Y_PLANE = 11 

459 AUTO_ALIGN = 12 

460 

461 

462class CoordDefinitionMode(Enum): 

463 """3D coordinate-definition mode.""" 

464 

465 OFF = 0 

466 ORIG_POINT_ON_AXIS_POINT_ON_PLANE = 1 

467 THREE_POINTS_ON_AXES = 2 

468 FREE_POINTS = 3 

469 

470 

471class ScanHeadType(Enum): 

472 """Scanning-head type.""" 

473 

474 NONE = 0 

475 OFV040 = 1 

476 OFV048_DOWN = 2 

477 OFV048_FRONT = 3 

478 OFV050 = 4 

479 OFV055 = 5 

480 OFV055F = 6 

481 OFV056 = 7 

482 MSV = 8 

483 VPI = 9 

484 PSVI400_MR = 10 

485 PSVI400_LR = 11 

486 MSV_NTSC = 12 

487 MSAI400 = 13 

488 MSAI500 = 14 

489 UHF120 = 15 

490 OFV534 = 16 

491 PSV500 = 17 

492 MSAI100 = 18 

493 MPVI800 = 19 

494 MSAI600 = 20 

495 MSAI620 = 21 

496 MSAI650 = 22 

497 PSV6XX = 23 

498 MSAI630 = 24 

499 VIO_I_130 = 25 

500 VFX_I_130 = 26 

501 PSVI7XX = 27 

502 

503 

504class ScanStatus(IntFlag): 

505 """Scan-point status flags.""" 

506 

507 NONE = 0 

508 VALID = 1 

509 OPTIMAL = 2 

510 OVERRANGE = 4 

511 INVALIDATED = 8 

512 DISABLED = 16 

513 INVALID_FRAMES = 32 

514 NOT_REACHABLE = 64 

515 HIDDEN = 128 

516 VIDEO_TRIANGULATION_FAILED = 256 

517 INTERPOLATE = 512 

518 INTERPOLATION_FAILED = 1024 

519 ASSIGNED_CHANNEL_1D = 2048 

520 ASSIGNED_CHANNEL_3D = 4096 

521 ASSIGNED_SENSOR = 8192 

522 

523 

524class FrontEndCaps(IntFlag): 

525 """Front-end capability flags.""" 

526 

527 CONTROLLABLE = 1 

528 DIFFERENTIAL_INPUT = 2 

529 FE_3D = 4 

530 MULTIPLE_SCAN_HEAD = 8 

531 VDD = 16 

532 FRINGE_COUNTER = 32 

533 S_VIDEO = 64 

534 IO_FRONT_END = 128 

535 FIXED_VIB_CHANNELS = 256 

536 REFERENCE_CHANNELS = 512 

537 DC_COUPLING_FOR_VIB_CHANNELS = 1024 

538 IEPE_ENFORCE_AC_COUPLING = 2048 

539 DIGITAL_VIDEO = 4096 

540 MULTI_POINT_VIB = 8192 

541 NO_HW_DECODER = 16384 

542 SINGLE_POINT_3D = 32768 

543 

544 

545class AcqBoard(Enum): 

546 """Acquisition-board type.""" 

547 

548 UNKNOWN = 0 

549 NONE = 1 

550 PC430 = 2 

551 SP216 = 3 

552 NI4451 = 4 

553 NI4452 = 5 

554 NI6110 = 6 

555 NI6111 = 7 

556 NI6110_20MHZ = 8 

557 NI6111_20MHZ = 9 

558 NI6601 = 10 

559 NI6602 = 11 

560 NI6601_20MHZ = 12 

561 NI6602_20MHZ = 13 

562 SIMULATION = 14 

563 NI6534 = 15 

564 NI6534_20MHZ = 16 

565 SPDIF = 17 

566 MI3025 = 18 

567 NI6115 = 19 

568 NI6115_20MHZ = 20 

569 NI4461 = 21 

570 NI4462 = 22 

571 VIB_E_220 = 23 

572 LECROY_WR204XI = 24 

573 M2I3027 = 25 

574 LECROY_WP725ZI = 26 

575 POLY_ETH = 27 

576 LECROY_WP725ZI_A = 28 

577 M2I4961 = 29 

578 MPV800 = 30 

579 LECROY_WR8254M = 31 

580 ADQ7 = 32 

581 M4I4450 = 33 

582 M2P5913 = 34 

583 M2P5943 = 35 

584 M2P5912 = 36 

585 M2P5942 = 37 

586 ROHDE_SCHWARZ_RTP = 38 

587 

588 

589class FrontEndType(Enum): 

590 """Front-end type.""" 

591 

592 UNKNOWN = 0 

593 NONE = 1 

594 BNC2140 = 2 

595 PSV_Z_010 = 3 

596 PSV_Z_040 = 4 

597 PSV_Z_040F = 5 

598 VIB_Z_010 = 6 

599 VDD_Z_010 = 7 

600 MSV_Z_040 = 8 

601 VIB_Z_014 = 9 

602 VIB_Z_015 = 10 

603 VDD_Z_011 = 11 

604 VIB_Z_016 = 12 

605 VIB_Z_012 = 13 

606 VIB_Z_017 = 14 

607 PSV_Z_040H = 15 

608 PSV_Z_040U = 16 

609 PSV_Z_3D_H = 17 

610 PSV_Z_1D_H = 18 

611 MSA_E_400_M2 = 19 

612 MSA_E_400_M4 = 20 

613 MSA_E_400_VDD = 21 

614 PSV_E_400_B = 22 

615 PSV_E_400_H4 = 23 

616 PSV_E_400_M2 = 24 

617 PSV_E_400_M2_20 = 25 

618 PSV_E_400_M4 = 26 

619 PSV_E_400_3D = 27 

620 PSV_E_400_3D_1D = 28 

621 MSA_E_400_M2_20 = 29 

622 PSV_E_400_3D_M4 = 30 

623 PSV_E_400_3D_1D_M4 = 31 

624 MSA_E_401_M2 = 32 

625 MSA_E_401_M4 = 33 

626 MSA_E_401_M2_20 = 34 

627 MSA_E_401_VDD = 35 

628 VDD_E_600 = 36 

629 PSV_E_401_B = 37 

630 PSV_E_401_H4 = 38 

631 PSV_E_401_M2 = 39 

632 PSV_E_401_M2_20 = 40 

633 PSV_E_401_M4 = 41 

634 PSV_E_401_3D = 42 

635 PSV_E_401_3D_1D = 43 

636 PSV_E_401_3D_M4 = 44 

637 PSV_E_401_3D_1D_M4 = 45 

638 MSA_E_401_4_M4 = 46 

639 PSV_E_400_1D = 47 

640 PSV_E_400_1D_M4 = 48 

641 PSV_E_401_1D = 49 

642 PSV_E_401_1D_M4 = 50 

643 PCI4461 = 51 

644 PCI4462 = 52 

645 VIB_E_220 = 53 

646 MSA_E_500_M2 = 54 

647 MSA_E_500_M4 = 55 

648 MSA_E_500_M2_20 = 56 

649 MSA_E_500_VDD = 57 

650 MSA_E_500_4_M4 = 58 

651 LECROY_WR204XI = 60 

652 LECROY_WP725ZI = 61 

653 VIB_E_400_80 = 62 

654 VIB_E_400_84 = 63 

655 VIB_E_400_1000 = 64 

656 VIB_E_400_1004 = 65 

657 VIB_E_400_VDD = 66 

658 VIB_E_400_3D_84 = 67 

659 VIB_E_400_3D_1004 = 68 

660 PSV_E_400_1D_M2_20 = 69 

661 PSV_E_401_1D_M2_20 = 70 

662 PSV_E_401_VDD = 71 

663 PSV_E_401_2_M4 = 72 

664 MSA_E_050_3D_1D_84 = 74 

665 MSA_E_050_1D_84 = 75 

666 MSA_E_050_3D_1004 = 76 

667 MSA_E_050_3D_1D_1004 = 77 

668 MSA_E_050_1D_1004 = 78 

669 PSV_F_500 = 79 

670 PSV_F_500_M = 80 

671 PSV_F_500_3D = 81 

672 PSV_F_500_3D_1D = 82 

673 PSV_F_500_1D = 83 

674 PSV_F_500_3D_M = 84 

675 PSV_F_500_3D_1D_M = 85 

676 PSV_F_500_1D_M = 86 

677 LECROY_WP725ZI_A = 87 

678 PSV_F_500_V = 88 

679 PSV_F_500_3D_V = 89 

680 PSV_F_500_3D_1D_V = 90 

681 PSV_F_500_1D_V = 91 

682 MSA_F_100_3D = 92 

683 MSA_F_100_3D_1D = 93 

684 MSA_F_100_1D = 94 

685 MSA_F_100_3D_M = 95 

686 MSA_F_100_3D_1D_M = 96 

687 MSA_F_100_1D_M = 97 

688 MSA_F_100_3D_V = 98 

689 MSA_F_100_3D_1D_V = 99 

690 MSA_F_100_1D_V = 100 

691 PSV_F_500_B = 101 

692 VIBSOFT_M2_40 = 102 

693 VIBSOFT_M4_25 = 103 

694 TMS_350 = 104 

695 MPV_E_800 = 105 

696 MSA_E_050_80 = 106 

697 MSA_E_050_84 = 107 

698 MSA_E_050_1000 = 108 

699 MSA_E_050_1004 = 109 

700 MSA_E_050_M2_20 = 110 

701 MSA_F_600_V = 111 

702 LECROY_WR8254M = 112 

703 MSA_F_600_M = 113 

704 VFX = 114 

705 IVS500 = 115 

706 MSA_F_620 = 116 

707 VGO200 = 117 

708 VIO130 = 118 

709 M4I4450 = 119 

710 MSA_F_650_M = 120 

711 MSA_F_650_V = 121 

712 VIBSOFT_M8_25 = 122 

713 PSV_F_600 = 123 

714 PSV_F_600_3D = 124 

715 PSV_F_600_3D_1D = 125 

716 PSV_F_600_1D = 126 

717 PSV_F_600_V = 127 

718 PSV_F_600_3D_V = 128 

719 PSV_F_600_3D_1D_V = 129 

720 PSV_F_600_1D_V = 130 

721 VGO200_P = 131 

722 MSA_F_630 = 132 

723 VIBSOFT_PRO = 133 

724 VIBSOFT_PRO_05 = 134 

725 VIBSOFT_PRO_3 = 135 

726 VIBSOFT_PRO_24 = 136 

727 VIBSOFT_PRO_3D = 137 

728 VIBSOFT_PRO_05_3D = 138 

729 VIBSOFT_PRO_3_3D = 139 

730 VIBSOFT_PRO_24_3D = 140 

731 MSA_E_060_PRO = 141 

732 MSA_E_060_PRO_05 = 142 

733 MSA_E_060_PRO_3 = 143 

734 MSA_E_060_PRO_24 = 144 

735 PSV_F_700 = 145 

736 PSV_F_700_3D = 146 

737 PSV_F_700_3D_1D = 147 

738 PSV_F_700_1D = 148 

739 PSV_I_780 = 149 

740 PSV_I_730 = 150 

741 VIBSOFT_PRO_3D_1D = 151 

742 VIBSOFT_PRO_05_3D_1D = 152 

743 VIBSOFT_PRO_3_3D_1D = 153 

744 VIBSOFT_PRO_24_3D_1D = 154 

745 

746 

747class GeneratorType(Enum): 

748 """Signal-generator type.""" 

749 

750 UNKNOWN = 0 

751 NONE = 1 

752 SIMULATION = 2 

753 HP33120A = 3 

754 HP33250A = 4 

755 PREMA1000 = 5 

756 NI4451 = 6 

757 NI611X = 7 

758 NI671X = 8 

759 MI60XX = 9 

760 NI673X = 10 

761 NI4461 = 11 

762 M2I60XX = 12 

763 POLY_ETHERNET = 13 

764 RS_SMBV100A = 14 

765 MPV800 = 15 

766 M4I6630 = 16 

767 M2P6570 = 17 

768 PSV700 = 18 

769 

770 

771class GeometryStatus(IntFlag): 

772 """3D geometry status flags.""" 

773 

774 NONE = 0 

775 VALID = 1 

776 MEASURED = 4 

777 IMPORTED = 8 

778 TRIANGULATION = 16 

779 INTERPOLATED = 32 

780 MODIFIED = 64 

781 TOO_BRIGHT = 128 

782 TOO_DARK = 256 

783 FAILED = 512 

784 OPTIMAL_MEASURED = 1024 

785 CALCULATED_3D_FROM_2D = 2048 

786 CALCULATED_2D_FROM_3D = 4096 

787 VIDEO_TRIANGULATED = 8192 

788 CALCULATED_3D_FROM_2D_AND_DISTANCE = 16384 

789 

790 

791class FocusStatus(IntFlag): 

792 """Laser-focus status flags.""" 

793 

794 NONE = 0 

795 VALID = 1 

796 MANUAL = 2 

797 FAST = 4 

798 BEST = 8 

799 INTERPOLATED = 16 

800 FAILED = 32 

801 CALCULATED = 64 

802 AUTO = 128 

803 

804 

805class MeasurementLocationMode(Enum): 

806 """Measurement-location mode.""" 

807 

808 FIXED = 0 

809 VARYING = 1 

810 

811 

812class ProfileType(Enum): 

813 """Profile type.""" 

814 

815 UNKNOWN = 0 

816 POLYGON = 1 

817 HORIZONTAL = 2 

818 VERTICAL = 4 

819 ORDER = 8 

820 

821 

822class DistanceSensorType(Enum): 

823 """Distance-sensor type.""" 

824 

825 NONE = 0 

826 SIMULATION = 1 

827 DLS_A_15 = 2 

828 DLS_A_15_FILTER = 3 

829 LSM215 = 4 

830 G500 = 5 

831 G600 = 6 

832 PSV700 = 7 

833 

834 

835class PreferredScanDirection(Enum): 

836 """Preferred scan direction.""" 

837 

838 NONE = 0 

839 X = 1 

840 Y = 2 

841 AUTO = 3 

842 INDEX = 4 

843 

844 

845class ScanHeadVideoType(IntFlag): 

846 """Scanning-head video capability/type flags.""" 

847 

848 NONE = 1 

849 REMOTE = 2 

850 COLOR = 4 

851 PAL = 8 

852 NTSC = 16 

853 DIGITAL = 32 

854 HD = 64 

855 

856 

857class ScanHeadCaps(IntFlag): 

858 """Scanning-head capability flags.""" 

859 

860 NONE = 0 

861 CONTROL = 1 

862 VIDEO = 2 

863 HAND_SET = 4 

864 PAN_TILT = 8 

865 LASER_FOCUS = 16 

866 LASER_AUTO_FOCUS = 32 

867 DISTANCE_SENSOR = 64 

868 MICROSCOPIC = 128 

869 DISPLAY = 256 

870 LASER_CONTROL = 512 

871 SCANNER_CONTROL = 1024 

872 HEADS_3_IN_ONE = 2048 

873 MULTIPLE_LENSES = 4096 

874 GREEN_LASER = 8192 

875 SPECKLE_TRACKING = 16384 

876 

877 

878class GraphicFormatType(Enum): 

879 """Graphics export format.""" 

880 

881 BITMAP = 0 

882 TIFF = 1 

883 PNG = 2 

884 TARGA = 3 

885 METAFILE = 4 

886 PHOTOSHOP30 = 5 

887 PCX = 6 

888 JPEG = 7 

889 POSTSCRIPT = 8 

890 WORDPERFECT = 9 

891 SUN = 10 

892 MACINTOSH_PICT = 11 

893 GIF = 12 

894 

895 

896class LensCalibrationCaps(IntFlag): 

897 """Lens-calibration capability flags.""" 

898 

899 INTERFERENCE = 1 

900 

901 

902class ChannelCaps(IntFlag): 

903 """Signal channel capability flags.""" 

904 

905 NONE = 0 

906 SCALAR = 1 

907 VECTOR = 2 

908 USER = 4 

909 X = 8 

910 Y = 16 

911 Z = 32 

912 D3_1D = 64 

913 RESPONSE = 128 

914 

915 

916class DomainType(Enum): 

917 """Signal domain type.""" 

918 

919 NOT_AVAIL = 0 

920 TIME = 1 

921 ANGLE_SYNC = 2 

922 SPECTRUM = 3 

923 THIRD_OCTAVE = 4 

924 ORDER_SPECTRUM = 5 

925 RMS = 6 

926 

927 

928class DisplayType(Enum): 

929 """Display type for a signal.""" 

930 

931 NOT_AVAIL = 0 

932 MAG = 1 

933 MAG_PHASE = 2 

934 MAG_DB = 3 

935 MAG_DB_PHASE = 4 

936 MAG_DB_A = 5 

937 PHASE = 6 

938 REAL = 7 

939 IMAG = 8 

940 REAL_IMAG = 9 

941 NYQUIST = 10 

942 INST_VAL = 11 

943 SAMPLES = 12 

944 IQ_FITTED = 13 

945 MAG_PHASE_RAD = 14 

946 MAG_DB_PHASE_RAD = 15 

947 

948 

949class DataType(Enum): 

950 """Signal data type.""" 

951 

952 UNKNOWN = 0 

953 POINT = 1 

954 AVERAGE = 2 

955 BAND = 3 

956 

957 

958class DOFDirection(Enum): 

959 """Degree-of-freedom direction.""" 

960 

961 SCALAR = 0 

962 PLUS_X_TRANSLATION = 1 

963 PLUS_Y_TRANSLATION = 2 

964 PLUS_Z_TRANSLATION = 3 

965 PLUS_X_ROTATION = 4 

966 PLUS_Y_ROTATION = 5 

967 PLUS_Z_ROTATION = 6 

968 VECTOR = 7 

969 MINUS_X_TRANSLATION = -1 

970 MINUS_Y_TRANSLATION = -2 

971 MINUS_Z_TRANSLATION = -3 

972 MINUS_X_ROTATION = -4 

973 MINUS_Y_ROTATION = -5 

974 MINUS_Z_ROTATION = -6 

975 

976 

977class FunctionType(Enum): 

978 """Signal function type.""" 

979 

980 UNKNOWN = 0 

981 TIME_RESPONSE = 1 

982 AUTO_SPECTRUM = 2 

983 CROSS_SPECTRUM = 3 

984 FREQUENCY_RESPONSE_FUNCTION = 4 

985 TRANSMISSIBILITY = 5 

986 COHERENCE = 6 

987 AUTO_CORRELATION = 7 

988 CROSS_CORRELATION = 8 

989 POWER_SPECTRAL_DENSITY = 9 

990 ENERGY_SPECTRAL_DENSITY = 10 

991 PROBABILITY_DENSITY_FUNCTION = 11 

992 SPECTRUM = 12 

993 CUMULATIVE_FREQUENCY_DISTRIBUTION = 13 

994 PEAKS_VALLEY = 14 

995 STRESS_CYCLES = 15 

996 STRAIN_CYCLES = 16 

997 ORBIT = 17 

998 MODE_INDICATOR_FUNCTION = 18 

999 FORCE_PATTERN = 19 

1000 PARTIAL_POWER = 20 

1001 PARTIAL_COHERENCE = 21 

1002 EIGENVALUE = 22 

1003 EIGENVECTOR = 23 

1004 SHOCK_RESPONSE_SPECTRUM = 24 

1005 FINITE_IMPULSE_RESPONSE_FILTER = 25 

1006 MULTIPLE_COHERENCE = 26 

1007 ORDER_FUNCTION = 27 

1008 IMPULSE_RESPONSE = 100 

1009 INTENSITY = 101 

1010 PI_INDEX = 102 

1011 SOUND_PRESSURE_LEVEL = 103 

1012 SOUND_POWER = 104 

1013 REAL_VALUED_DATA = 105 

1014 COMPLEX_VALUED_DATA = 106 

1015 PARTICLE_VELOCITY = 107 

1016 TEMPERATURE = 108 

1017 FREQUENCY_RESPONSE_FUNCTION_H1 = 300 

1018 FREQUENCY_RESPONSE_FUNCTION_H2 = 301 

1019 FREQUENCY_RESPONSE_FUNCTION_PCA_H1 = 302 

1020 PCA_PRINCIPAL_INPUTS = 303 

1021 PCA_VIRTUAL_COHERENCES = 304 

1022 VDD_IQ = 305 

1023 BAND = 306 

1024 

1025 

1026def _flags_list(value: int, flag_enum: type[_FlagT], *, include_none: bool = False) -> list[_FlagT]: 

1027 """Return the list of set flags for an ``IntFlag`` enum value.""" 

1028 flags = flag_enum(int(value)) 

1029 out: list[_FlagT] = [] 

1030 zero = flag_enum(0) 

1031 for flag in flag_enum: 

1032 if flag == zero: 

1033 if include_none: 

1034 out.append(flag) 

1035 continue 

1036 if flags & flag: 

1037 out.append(flag) 

1038 return out 

1039 

1040 

1041def _unwrap_com(obj: Any) -> Any: 

1042 """Return the raw COM object if ``obj`` is one of this module's wrappers.""" 

1043 return obj._com if hasattr(obj, "_com") else obj 

1044 

1045 

1046def _build_linear_abscissa(x_axis: Any) -> np.ndarray: 

1047 """Build a linearly spaced x-axis array from a COM x-axis descriptor.""" 

1048 count = int(x_axis.max_count) 

1049 if count <= 0: 

1050 return np.array([], dtype=float) 

1051 if count == 1: 

1052 return np.array([float(x_axis.min)], dtype=float) 

1053 step = (x_axis.max - x_axis.min) / (count - 1) 

1054 return np.arange(count, dtype=float) * step + x_axis.min 

1055 

1056 

1057def _find_display_by_name(signal: Any, display_name: str) -> Any: 

1058 """Return the display with the requested name or raise ``ValueError``.""" 

1059 for display in signal.displays: 

1060 if display.name == display_name: 

1061 return display 

1062 raise ValueError(f"No display named {display_name!r} found for signal " f"{signal.name!r}.") 

1063 

1064 

1065class AverageProperties: 

1066 """Wrapper around COM average settings.""" 

1067 

1068 def __init__(self, obj: Any) -> None: 

1069 self._com = obj 

1070 

1071 @property 

1072 def count(self) -> int: 

1073 """Number of averages in the measurement.""" 

1074 return int(self._com.Count) 

1075 

1076 @property 

1077 def type(self) -> AverageType: 

1078 """Averaging type used for the measurement.""" 

1079 return AverageType(self._com.Type) 

1080 

1081 

1082class DigitalFilter: 

1083 """Wrapper around COM digital filter settings.""" 

1084 

1085 def __init__(self, obj: Any) -> None: 

1086 self._com = obj 

1087 

1088 @property 

1089 def type(self) -> DigitalFilterType: 

1090 """Filter type.""" 

1091 return DigitalFilterType(self._com.Type) 

1092 

1093 @property 

1094 def quality(self) -> DigitalFilterQuality: 

1095 """Filter quality setting.""" 

1096 return DigitalFilterQuality(self._com.Quality) 

1097 

1098 @property 

1099 def cutoff_freq(self) -> float | tuple[float, float]: 

1100 """Cutoff frequency or frequencies for the configured filter. 

1101 

1102 Returns 

1103 ------- 

1104 float | tuple[float, float] 

1105 A single cutoff frequency for low-pass and high-pass filters, or a 

1106 pair of cutoff frequencies for band-pass and notch filters. 

1107 

1108 Raises 

1109 ------ 

1110 ValueError 

1111 If the filter type does not define a cutoff frequency. 

1112 """ 

1113 filter_type = self.type 

1114 if filter_type in (DigitalFilterType.LOWPASS, DigitalFilterType.HIGHPASS): 

1115 return float(self._com.CutoffFreq) 

1116 if filter_type in (DigitalFilterType.BANDPASS, DigitalFilterType.NOTCH): 

1117 return (float(self._com.CutoffFreq1), float(self._com.CutoffFreq2)) 

1118 raise ValueError("Filter has no cutoff frequency defined.") 

1119 

1120 

1121class ChannelsProperties: 

1122 """Wrapper around COM acquisition channel properties.""" 

1123 

1124 def __init__(self, obj: Any) -> None: 

1125 self._com = obj 

1126 

1127 @property 

1128 def active(self) -> bool: 

1129 """Whether the channel is active.""" 

1130 return bool(self._com.Active) 

1131 

1132 @property 

1133 def calibration(self) -> float: 

1134 """Calibration factor in SI units.""" 

1135 return float(self._com.Calibration) 

1136 

1137 @property 

1138 def differential_input(self) -> bool: 

1139 """Whether the channel expects differential input.""" 

1140 return bool(self._com.DifferentialInput) 

1141 

1142 @property 

1143 def digital_filter(self) -> DigitalFilter: 

1144 """Digital filter applied to the input signal.""" 

1145 return DigitalFilter(self._com.DigitalFilter) 

1146 

1147 @property 

1148 def direction(self) -> Direction: 

1149 """Direction of vibration for this channel.""" 

1150 return Direction(self._com.DirectionOfVibration) 

1151 

1152 @property 

1153 def icp_input(self) -> bool: 

1154 """Whether the channel is configured for an ICP device.""" 

1155 return bool(self._com.ICPInput) 

1156 

1157 @property 

1158 def coupling(self) -> Coupling: 

1159 """Input coupling for the channel.""" 

1160 return Coupling(self._com.InputCoupling) 

1161 

1162 @property 

1163 def impedance(self) -> float: 

1164 """Input impedance in ohms.""" 

1165 return float(self._com.InputImpedance) 

1166 

1167 @property 

1168 def range(self) -> float: 

1169 """Input range in volts.""" 

1170 return float(self._com.InputRange) 

1171 

1172 @property 

1173 def int_diff_physical_quantity(self) -> PhysicalQuantity: 

1174 """Quantity produced by integration/differentiation filtering.""" 

1175 return PhysicalQuantity(self._com.IntDiffPhysicalQuantity) 

1176 

1177 @property 

1178 def name(self) -> str: 

1179 """Channel descriptor.""" 

1180 return self._com.Name 

1181 

1182 @property 

1183 def quantity(self) -> PhysicalQuantity: 

1184 """Physical quantity of the channel.""" 

1185 return PhysicalQuantity(self._com.Quantity) 

1186 

1187 @property 

1188 def reference(self) -> bool: 

1189 """Whether the channel is marked as a reference.""" 

1190 return bool(self._com.Reference) 

1191 

1192 @property 

1193 def node_id(self) -> int: 

1194 """One-based scan-point index associated with the channel.""" 

1195 return int(self._com.ReferencePointIndex) 

1196 

1197 @property 

1198 def signal_enhancement(self) -> bool: 

1199 """Whether signal enhancement is active for the channel.""" 

1200 return bool(self._com.SEActive) 

1201 

1202 @property 

1203 def short_name(self) -> str: 

1204 """Abbreviated channel descriptor.""" 

1205 return self._com.ShortName 

1206 

1207 @property 

1208 def signal_delay(self) -> float: 

1209 """Signal delay in seconds.""" 

1210 return float(self._com.SignalDelay) 

1211 

1212 @property 

1213 def type(self) -> ChannelType: 

1214 """Signal type of the channel.""" 

1215 return ChannelType(self._com.Type) 

1216 

1217 @property 

1218 def unit(self) -> str: 

1219 """Engineering unit of the channel quantity.""" 

1220 return self._com.Unit 

1221 

1222 @property 

1223 def vibrometer_index(self) -> int: 

1224 """Index of the vibrometer connected to this channel, or 0 if none.""" 

1225 return int(self._com.VibrometerIndex) 

1226 

1227 @property 

1228 def window_function(self) -> WindowFunction: 

1229 """Window function used for the channel.""" 

1230 return WindowFunction(self._com.WindowFunction) 

1231 

1232 @property 

1233 def window_function_parameters(self) -> Any: 

1234 """Parameters of the configured window function.""" 

1235 return self._com.WindowFunctionParams 

1236 

1237 @property 

1238 def window_function_rms_correction(self) -> float: 

1239 """RMS correction factor for the configured window function.""" 

1240 return float(self._com.WindowFunctionRMSCorrection) 

1241 

1242 

1243class FastScanProperties: 

1244 """Wrapper around COM FastScan acquisition properties.""" 

1245 

1246 def __init__(self, obj: Any) -> None: 

1247 self._com = obj 

1248 

1249 @property 

1250 def bandwidth(self) -> float: 

1251 """FastScan bandwidth in hertz.""" 

1252 return float(self._com.Bandwidth) 

1253 

1254 @property 

1255 def frequency(self) -> float: 

1256 """FastScan frequency in hertz.""" 

1257 return float(self._com.Frequency) 

1258 

1259 @property 

1260 def sample_frequency(self) -> float: 

1261 """Sampling frequency in hertz.""" 

1262 return float(self._com.SampleFrequency) 

1263 

1264 @property 

1265 def samples(self) -> int: 

1266 """Number of samples in the FastScan acquisition.""" 

1267 return int(self._com.Samples) 

1268 

1269 @property 

1270 def sample_time(self) -> float: 

1271 """Sample time in seconds.""" 

1272 return float(self._com.SampleTime) 

1273 

1274 

1275class FFTProperties: 

1276 """Wrapper around COM FFT acquisition properties.""" 

1277 

1278 def __init__(self, obj: Any) -> None: 

1279 self._com = obj 

1280 

1281 @property 

1282 def bandwidth(self) -> float: 

1283 """FFT bandwidth in hertz.""" 

1284 return float(self._com.Bandwidth) 

1285 

1286 @property 

1287 def end_frequency(self) -> float: 

1288 """End frequency in hertz.""" 

1289 return float(self._com.EndFrequency) 

1290 

1291 @property 

1292 def lines(self) -> int: 

1293 """Number of FFT lines.""" 

1294 return int(self._com.Lines) 

1295 

1296 @property 

1297 def overlap(self) -> float: 

1298 """Overlap in percent.""" 

1299 return float(self._com.Overlap) 

1300 

1301 @property 

1302 def sample_frequency(self) -> float: 

1303 """Sampling frequency in hertz.""" 

1304 return float(self._com.SampleFrequency) 

1305 

1306 @property 

1307 def sample_resolution(self) -> float: 

1308 """Frequency resolution in hertz.""" 

1309 return float(self._com.SampleResolution) 

1310 

1311 @property 

1312 def samples(self) -> int: 

1313 """Number of samples used for the FFT.""" 

1314 return int(self._com.Samples) 

1315 

1316 @property 

1317 def sample_time(self) -> float: 

1318 """Sample time in seconds.""" 

1319 return float(self._com.SampleTime) 

1320 

1321 @property 

1322 def start_frequency(self) -> float: 

1323 """Start frequency in hertz.""" 

1324 return float(self._com.StartFrequency) 

1325 

1326 

1327class FrontEndProperties: 

1328 """Wrapper around COM front-end acquisition properties.""" 

1329 

1330 def __init__(self, obj: Any) -> None: 

1331 self._com = obj 

1332 

1333 @property 

1334 def generator_amplifier_enabled(self) -> bool: 

1335 """Whether the generator amplifier is enabled.""" 

1336 return bool(self._com.GeneratorAmplifierEnabled) 

1337 

1338 @property 

1339 def generator_amplifier_factor(self) -> float: 

1340 """Amplification factor of the generator amplifier.""" 

1341 return float(self._com.GeneratorAmplifierFactor) 

1342 

1343 

1344class GeneralProperties: 

1345 """Wrapper around COM general acquisition properties.""" 

1346 

1347 def __init__(self, obj: Any) -> None: 

1348 self._com = obj 

1349 

1350 @property 

1351 def auto_remeasure(self) -> bool: 

1352 """Whether auto remeasure is enabled.""" 

1353 return bool(self._com.AutoRemeasure) 

1354 

1355 @property 

1356 def auto_remeasure_auto_range(self) -> bool: 

1357 """Whether vibrometer range is automatically adjusted on overrange.""" 

1358 return bool(self._com.AutoRemeasureAutoRange) 

1359 

1360 @property 

1361 def bandwidth_extension(self) -> BandwidthExtension: 

1362 """Bandwidth extension setting for the UHF vibrometer.""" 

1363 return BandwidthExtension(self._com.BandWidthExtension) 

1364 

1365 @property 

1366 def principal_component_analysis(self) -> bool: 

1367 """Whether principal component analysis (MIMO) is enabled.""" 

1368 return bool(self._com.PrincipalComponentAnalysis) 

1369 

1370 @property 

1371 def real_sample_frequency(self) -> float: 

1372 """Raw sampling frequency before digital decimation.""" 

1373 return float(self._com.RealSampleFrequency) 

1374 

1375 

1376class GeneratorsProperties: 

1377 """Wrapper around COM signal-generator properties.""" 

1378 

1379 def __init__(self, obj: Any) -> None: 

1380 self._com = obj 

1381 

1382 @property 

1383 def active(self) -> bool: 

1384 """Whether the generator is active.""" 

1385 return bool(self._com.Active) 

1386 

1387 @property 

1388 def amplitude(self) -> float: 

1389 """Generator amplitude in volts.""" 

1390 return float(self._com.Amplitude) 

1391 

1392 @property 

1393 def amplitude_correction_data(self) -> Any: 

1394 """Amplitude-correction data.""" 

1395 return self._com.AmplitudeCorrectionData 

1396 

1397 @property 

1398 def multiple_channels(self) -> bool: 

1399 """Whether the generator uses multiple output channels.""" 

1400 return bool(self._com.MultipleChannels) 

1401 

1402 @property 

1403 def offset(self) -> float: 

1404 """Generator DC offset in volts.""" 

1405 return float(self._com.Offset) 

1406 

1407 @property 

1408 def steady_state_time(self) -> float: 

1409 """Waiting time for steady-state conditions in seconds.""" 

1410 return float(self._com.SteadyStateTime) 

1411 

1412 @property 

1413 def supports_multiple_channels(self) -> bool: 

1414 """Whether the generator supports multiple output channels.""" 

1415 return bool(self._com.SupportsMultipleChannels) 

1416 

1417 @property 

1418 def use_amplitude_correction(self) -> bool: 

1419 """Whether amplitude-correction data is used.""" 

1420 return bool(self._com.UseAmplitudeCorrection) 

1421 

1422 @property 

1423 def waveform(self) -> "Waveform": 

1424 """Configured waveform.""" 

1425 return make_waveform(self._com.Waveform) 

1426 

1427 

1428class Waveform: 

1429 """Base wrapper for generator waveform settings.""" 

1430 

1431 def __init__(self, obj: Any) -> None: 

1432 self._com = obj 

1433 

1434 @property 

1435 def name(self) -> str: 

1436 """Waveform name.""" 

1437 return self._com.Name 

1438 

1439 @property 

1440 def type(self) -> WaveformType: 

1441 """Waveform type.""" 

1442 return WaveformType(self._com.Type) 

1443 

1444 

1445class BurstChirpWaveform(Waveform): 

1446 """Burst-chirp waveform settings.""" 

1447 

1448 @property 

1449 def burst_length(self) -> float: 

1450 """Burst length in percent.""" 

1451 return float(self._com.BurstLength) 

1452 

1453 @property 

1454 def burst_length_ex(self) -> float: 

1455 """Extended burst length in percent.""" 

1456 return float(self._com.BurstLengthEx) 

1457 

1458 @property 

1459 def burst_start(self) -> float: 

1460 """Burst start in percent.""" 

1461 return float(self._com.BurstStart) 

1462 

1463 @property 

1464 def burst_start_ex(self) -> float: 

1465 """Extended burst start in percent.""" 

1466 return float(self._com.BurstStartEx) 

1467 

1468 @property 

1469 def end_frequency(self) -> float: 

1470 """End frequency in hertz.""" 

1471 return float(self._com.EndFrequency) 

1472 

1473 @property 

1474 def start_frequency(self) -> float: 

1475 """Start frequency in hertz.""" 

1476 return float(self._com.StartFrequency) 

1477 

1478 

1479class BurstRandomWaveform(Waveform): 

1480 """Burst-random waveform settings.""" 

1481 

1482 @property 

1483 def burst_length(self) -> float: 

1484 """Burst length in percent.""" 

1485 return float(self._com.BurstLength) 

1486 

1487 @property 

1488 def burst_length_ex(self) -> float: 

1489 """Extended burst length in percent.""" 

1490 return float(self._com.BurstLengthEx) 

1491 

1492 @property 

1493 def burst_start(self) -> float: 

1494 """Burst start in percent.""" 

1495 return float(self._com.BurstStart) 

1496 

1497 @property 

1498 def burst_start_ex(self) -> float: 

1499 """Extended burst start in percent.""" 

1500 return float(self._com.BurstStartEx) 

1501 

1502 

1503class MultiCarrierCWWaveform(Waveform): 

1504 """Multi-carrier continuous-wave waveform settings.""" 

1505 

1506 @property 

1507 def end_frequency(self) -> float: 

1508 """End frequency in hertz.""" 

1509 return float(self._com.EndFrequency) 

1510 

1511 @property 

1512 def start_frequency(self) -> float: 

1513 """Start frequency in hertz.""" 

1514 return float(self._com.StartFrequency) 

1515 

1516 

1517class PeriodicChirpWaveform(Waveform): 

1518 """Periodic-chirp waveform.""" 

1519 

1520 

1521class PseudoRandomWaveform(Waveform): 

1522 """Pseudo-random waveform.""" 

1523 

1524 

1525class RampWaveform(Waveform): 

1526 """Ramp waveform settings.""" 

1527 

1528 @property 

1529 def frequency(self) -> float: 

1530 """Waveform frequency in hertz.""" 

1531 return float(self._com.Frequency) 

1532 

1533 

1534class SineWaveform(Waveform): 

1535 """Sine waveform settings.""" 

1536 

1537 @property 

1538 def frequency(self) -> float: 

1539 """Waveform frequency in hertz.""" 

1540 return float(self._com.Frequency) 

1541 

1542 

1543class SquareWaveform(Waveform): 

1544 """Square waveform settings.""" 

1545 

1546 @property 

1547 def frequency(self) -> float: 

1548 """Waveform frequency in hertz.""" 

1549 return float(self._com.Frequency) 

1550 

1551 

1552class SweepWaveform(Waveform): 

1553 """Sweep waveform settings.""" 

1554 

1555 @property 

1556 def end_frequency(self) -> float: 

1557 """End frequency of the sweep in hertz.""" 

1558 return float(self._com.EndFrequency) 

1559 

1560 @property 

1561 def start_frequency(self) -> float: 

1562 """Start frequency of the sweep in hertz.""" 

1563 return float(self._com.StartFrequency) 

1564 

1565 @property 

1566 def sweep_time(self) -> float: 

1567 """Sweep duration in seconds.""" 

1568 return float(self._com.SweepTime) 

1569 

1570 

1571class TriangleWaveform(Waveform): 

1572 """Triangle waveform settings.""" 

1573 

1574 @property 

1575 def frequency(self) -> float: 

1576 """Waveform frequency in hertz.""" 

1577 return float(self._com.Frequency) 

1578 

1579 

1580class WhiteNoiseWaveform(Waveform): 

1581 """White-noise waveform.""" 

1582 

1583 

1584class UserDefinedWaveform(Waveform): 

1585 """User-defined waveform settings.""" 

1586 

1587 @property 

1588 def frequency(self) -> float: 

1589 """Waveform frequency in hertz.""" 

1590 return float(self._com.Frequency) 

1591 

1592 @property 

1593 def waveform_data(self) -> Any: 

1594 """Waveform data for the user-defined waveform.""" 

1595 return self._com.WaveformData 

1596 

1597 

1598def make_waveform(waveform_com_obj: Any) -> Waveform: 

1599 """Wrap a COM waveform object in the appropriate Python subclass.""" 

1600 wf_type = WaveformType(waveform_com_obj.Type) 

1601 

1602 cls_map: dict[WaveformType, type[Waveform]] = { 

1603 WaveformType.SINE: SineWaveform, 

1604 WaveformType.SQUARE: SquareWaveform, 

1605 WaveformType.TRIANGLE: TriangleWaveform, 

1606 WaveformType.RAMP: RampWaveform, 

1607 WaveformType.SWEEP: SweepWaveform, 

1608 WaveformType.BURST_CHIRP: BurstChirpWaveform, 

1609 WaveformType.BURST_RANDOM: BurstRandomWaveform, 

1610 WaveformType.PERIODIC_CHIRP: PeriodicChirpWaveform, 

1611 WaveformType.PSEUDORANDOM: PseudoRandomWaveform, 

1612 WaveformType.WHITE_NOISE: WhiteNoiseWaveform, 

1613 WaveformType.USER_DEFINED: UserDefinedWaveform, 

1614 WaveformType.MULTI_CARRIER_CW: MultiCarrierCWWaveform, 

1615 } 

1616 

1617 cls = cls_map.get(wf_type, Waveform) 

1618 return cls(waveform_com_obj) 

1619 

1620 

1621class MultiFrameProperties: 

1622 """Wrapper around COM multi-frame acquisition properties.""" 

1623 

1624 def __init__(self, obj: Any) -> None: 

1625 self._com = obj 

1626 

1627 @property 

1628 def average_type(self) -> AverageType: 

1629 """Average type used to average frames.""" 

1630 return AverageType(self._com.AverageType) 

1631 

1632 @property 

1633 def frame_count(self) -> int: 

1634 """Number of frames.""" 

1635 return int(self._com.FrameCount) 

1636 

1637 @property 

1638 def mode(self) -> MultiFrameMode: 

1639 """Multi-frame mode.""" 

1640 return MultiFrameMode(self._com.Mode) 

1641 

1642 @property 

1643 def pulses_per_cycle(self) -> int: 

1644 """Pulses per engine cycle used in automatic frame mode.""" 

1645 return int(self._com.PulsesPerCycle) 

1646 

1647 

1648class SignalEnhancementProperties: 

1649 """Wrapper around COM signal-enhancement properties.""" 

1650 

1651 def __init__(self, obj: Any) -> None: 

1652 self._com = obj 

1653 

1654 @property 

1655 def mode(self) -> SignalEnhancementMode: 

1656 """Signal-enhancement mode.""" 

1657 return SignalEnhancementMode(self._com.Mode) 

1658 

1659 @property 

1660 def speckle_tracking(self) -> bool: 

1661 """Whether speckle tracking is active.""" 

1662 return bool(self._com.SpeckleTracking) 

1663 

1664 

1665class TimeProperties: 

1666 """Wrapper around COM time-acquisition properties.""" 

1667 

1668 def __init__(self, obj: Any) -> None: 

1669 self._com = obj 

1670 

1671 @property 

1672 def sample_frequency(self) -> float: 

1673 """Sample frequency in hertz.""" 

1674 return float(self._com.SampleFrequency) 

1675 

1676 @property 

1677 def sample_resolution(self) -> float: 

1678 """Time resolution in seconds.""" 

1679 return float(self._com.SampleResolution) 

1680 

1681 @property 

1682 def samples(self) -> int: 

1683 """Number of samples.""" 

1684 return int(self._com.Samples) 

1685 

1686 @property 

1687 def sample_time(self) -> float: 

1688 """Sample time in seconds.""" 

1689 return float(self._com.SampleTime) 

1690 

1691 

1692class TriggerProperties: 

1693 """Wrapper around COM trigger properties.""" 

1694 

1695 def __init__(self, obj: Any) -> None: 

1696 self._com = obj 

1697 

1698 @property 

1699 def analog_source_channel(self) -> int: 

1700 """Source channel for analog triggering.""" 

1701 return int(self._com.AnalogSourceChannel) 

1702 

1703 @property 

1704 def edge(self) -> TriggerEdge: 

1705 """Trigger edge.""" 

1706 return TriggerEdge(self._com.Edge) 

1707 

1708 @property 

1709 def level(self) -> float: 

1710 """Trigger level in percent.""" 

1711 return float(self._com.Level) 

1712 

1713 @property 

1714 def phase_from_ref(self) -> bool: 

1715 """Whether phase is determined from the first reference FRF.""" 

1716 return bool(self._com.PhaseFromRef) 

1717 

1718 @property 

1719 def pre_trigger(self) -> float: 

1720 """Pre-trigger value in percent.""" 

1721 return float(self._com.PreTrigger) 

1722 

1723 @property 

1724 def pre_trigger_max(self) -> float: 

1725 """Maximum pre-trigger value in percent.""" 

1726 return float(self._com.PreTriggerMax) 

1727 

1728 @property 

1729 def pre_trigger_min(self) -> float: 

1730 """Minimum pre-trigger value in percent.""" 

1731 return float(self._com.PreTriggerMin) 

1732 

1733 @property 

1734 def pre_trigger_possible(self) -> bool: 

1735 """Whether pre-trigger is possible for the current settings.""" 

1736 return bool(self._com.PreTriggerPossible) 

1737 

1738 @property 

1739 def source(self) -> TriggerSource: 

1740 """Trigger source.""" 

1741 return TriggerSource(self._com.Source) 

1742 

1743 

1744class ControllerInfo: 

1745 """Wrapper around COM controller-information settings.""" 

1746 

1747 def __init__(self, obj: Any) -> None: 

1748 self._com = obj 

1749 

1750 @property 

1751 def caps_value(self) -> int: 

1752 """Controller capabilities as a raw integer bitmask.""" 

1753 return int(self._com.Caps) 

1754 

1755 @property 

1756 def caps(self) -> ControllerCaps: 

1757 """Controller capabilities as an ``IntFlag``.""" 

1758 return decode_controller_caps(self.caps_value) 

1759 

1760 @property 

1761 def caps_set(self) -> list[ControllerCaps]: 

1762 """Individual controller capability flags that are set.""" 

1763 return controller_caps_list(self.caps_value) 

1764 

1765 def has_cap(self, cap: ControllerCaps) -> bool: 

1766 """Return whether the controller supports the given capability flag(s).""" 

1767 return bool(self.caps & cap) 

1768 

1769 @property 

1770 def device_name(self) -> str: 

1771 """General controller device name.""" 

1772 return self._com.DeviceName 

1773 

1774 @property 

1775 def firmware_version(self) -> str: 

1776 """Controller firmware version.""" 

1777 return self._com.FirmwareVersion 

1778 

1779 @property 

1780 def name(self) -> str: 

1781 """Controller type/name.""" 

1782 return self._com.Name 

1783 

1784 @property 

1785 def serial_number(self) -> str: 

1786 """Controller serial number.""" 

1787 return self._com.SerialNumber 

1788 

1789 @property 

1790 def unique_id(self) -> str: 

1791 """Controller unique identifier.""" 

1792 return self._com.UniqueID 

1793 

1794 

1795class VideoCameraSettings: 

1796 """Wrapper around COM video-camera settings.""" 

1797 

1798 def __init__(self, obj: Any) -> None: 

1799 self._com = obj 

1800 

1801 @property 

1802 def automatic_gain_control(self) -> bool: 

1803 """Whether automatic gain control is active.""" 

1804 return bool(self._com.AutomaticGainControl) 

1805 

1806 @property 

1807 def gamma(self) -> float: 

1808 """Gamma value.""" 

1809 return float(self._com.Gamma) 

1810 

1811 @property 

1812 def manual_gain(self) -> float: 

1813 """Manual gain value.""" 

1814 return float(self._com.ManualGain) 

1815 

1816 @property 

1817 def mirror(self) -> bool: 

1818 """Mirror mode state.""" 

1819 return bool(self._com.Mirror) 

1820 

1821 @property 

1822 def shutter_speed(self) -> float: 

1823 """Shutter speed.""" 

1824 return float(self._com.ShutterSpeed) 

1825 

1826 

1827class SensorHeadInfo: 

1828 """Wrapper around COM sensor-head information.""" 

1829 

1830 def __init__(self, obj: Any) -> None: 

1831 self._com = obj 

1832 

1833 @property 

1834 def beam_gap(self) -> Any: 

1835 """Beam gap, including units when provided by the COM interface.""" 

1836 return self._com.BeamGap 

1837 

1838 @property 

1839 def bragg_cell_frequency(self) -> float: 

1840 """Bragg-cell frequency.""" 

1841 return float(self._com.BraggCellFrequency) 

1842 

1843 @property 

1844 def caps_value(self) -> int: 

1845 """Sensor-head capabilities as a raw integer bitmask.""" 

1846 return int(self._com.Caps) 

1847 

1848 @property 

1849 def caps(self) -> SensorHeadCaps: 

1850 """Sensor-head capabilities as an ``IntFlag``.""" 

1851 return SensorHeadCaps(self.caps_value) 

1852 

1853 @property 

1854 def caps_set(self) -> list[SensorHeadCaps]: 

1855 """Individual sensor-head capability flags that are set.""" 

1856 return _flags_list(self.caps_value, SensorHeadCaps) 

1857 

1858 @property 

1859 def caps2_value(self) -> int: 

1860 """Extended sensor-head capabilities as a raw integer bitmask.""" 

1861 return int(self._com.Caps2) 

1862 

1863 @property 

1864 def caps2(self) -> SensorHeadCaps2: 

1865 """Extended sensor-head capabilities as an ``IntFlag``.""" 

1866 return SensorHeadCaps2(self.caps2_value) 

1867 

1868 @property 

1869 def caps2_set(self) -> list[SensorHeadCaps2]: 

1870 """Individual extended sensor-head capability flags that are set.""" 

1871 return _flags_list(self.caps2_value, SensorHeadCaps2) 

1872 

1873 def has_cap(self, cap: SensorHeadCaps) -> bool: 

1874 """Return whether the sensor head supports the given capability flag(s).""" 

1875 return bool(self.caps & cap) 

1876 

1877 def has_cap2(self, cap: SensorHeadCaps2) -> bool: 

1878 """Return whether the sensor head supports the given extended capability flag(s).""" 

1879 return bool(self.caps2 & cap) 

1880 

1881 @property 

1882 def coherence_optimizer_firmware_version(self) -> str: 

1883 """Firmware version of the coherence optimizer.""" 

1884 return self._com.CoherenceOptimizerFirmwareVersion 

1885 

1886 @property 

1887 def connected(self) -> bool: 

1888 """Whether the sensor head is connected.""" 

1889 return bool(self._com.Connected) 

1890 

1891 @property 

1892 def dimmer_max(self) -> float: 

1893 """Maximum laser-beam transmission.""" 

1894 return float(self._com.DimmerMax) 

1895 

1896 @property 

1897 def dimmer_min(self) -> float: 

1898 """Minimum laser-beam transmission.""" 

1899 return float(self._com.DimmerMin) 

1900 

1901 @property 

1902 def firmware_version(self) -> str: 

1903 """Sensor-head firmware version.""" 

1904 return self._com.FirmwareVersion 

1905 

1906 @property 

1907 def focus_far_limit(self) -> float: 

1908 """Farthest focus position.""" 

1909 return float(self._com.FocusFarLimit) 

1910 

1911 @property 

1912 def focus_near_limit(self) -> float: 

1913 """Nearest focus position.""" 

1914 return float(self._com.FocusNearLimit) 

1915 

1916 @property 

1917 def illumination_intensity_max(self) -> float: 

1918 """Maximum illumination intensity.""" 

1919 return float(self._com.IlluminationIntensityMax) 

1920 

1921 @property 

1922 def illumination_intensity_min(self) -> float: 

1923 """Minimum illumination intensity.""" 

1924 return float(self._com.IlluminationIntensityMin) 

1925 

1926 @property 

1927 def laser_delay(self) -> float: 

1928 """Laser delay in seconds.""" 

1929 return float(self._com.LaserDelay) 

1930 

1931 @property 

1932 def laser_firmware_version(self) -> str: 

1933 """Laser firmware version string.""" 

1934 return self._com.LaserFirmwareVersion 

1935 

1936 @property 

1937 def laser_power(self) -> float: 

1938 """Laser power in milliwatts.""" 

1939 return float(self._com.LaserPower) 

1940 

1941 @property 

1942 def laser_wavelength(self) -> float: 

1943 """Laser wavelength in nanometers.""" 

1944 return float(self._com.LaserWavelength) 

1945 

1946 @property 

1947 def lower_objective_threshold_temperature(self) -> float: 

1948 """Lower objective-threshold temperature.""" 

1949 return float(self._com.LowerObjectiveThresholdTemperature) 

1950 

1951 @property 

1952 def max_rgb_led_color_index(self) -> int: 

1953 """Maximum RGB LED color index.""" 

1954 return int(self._com.MaxRGBLedColorIndex) 

1955 

1956 @property 

1957 def max_signal_balance(self) -> SignalBalanceState: 

1958 """Upper end of the signal-balance range.""" 

1959 return SignalBalanceState(self._com.MaxSignalBalance) 

1960 

1961 @property 

1962 def min_rgb_led_color_index(self) -> int: 

1963 """Minimum RGB LED color index.""" 

1964 return int(self._com.MinRGBLedColorIndex) 

1965 

1966 @property 

1967 def min_signal_balance(self) -> SignalBalanceState: 

1968 """Lower end of the signal-balance range.""" 

1969 return SignalBalanceState(self._com.MinSignalBalance) 

1970 

1971 @property 

1972 def name(self) -> str: 

1973 """Sensor-head name.""" 

1974 return self._com.Name 

1975 

1976 @property 

1977 def serial_number(self) -> str: 

1978 """Sensor-head serial number.""" 

1979 return self._com.SerialNumber 

1980 

1981 @property 

1982 def sonde_name(self) -> str: 

1983 """Sonde name.""" 

1984 return self._com.SondeName 

1985 

1986 @property 

1987 def standoff_dist(self) -> Any: 

1988 """Stand-off distance, including units when provided by the COM interface.""" 

1989 return self._com.StandoffDist 

1990 

1991 @property 

1992 def system_laser_power(self) -> float: 

1993 """System laser power in milliwatts.""" 

1994 return float(self._com.SystemLaserPower) 

1995 

1996 @property 

1997 def unique_id(self) -> str: 

1998 """Unique sensor-head identifier.""" 

1999 return self._com.UniqueID 

2000 

2001 @property 

2002 def upper_objective_threshold_temperature(self) -> float: 

2003 """Upper objective-threshold temperature.""" 

2004 return float(self._com.UpperObjectiveThresholdTemperature) 

2005 

2006 

2007class SensorHeadSettings: 

2008 """Wrapper around COM sensor-head settings.""" 

2009 

2010 def __init__(self, obj: Any) -> None: 

2011 self._com = obj 

2012 

2013 @property 

2014 def dimmer(self) -> float: 

2015 """Dimmer value.""" 

2016 return float(self._com.Dimmer) 

2017 

2018 @property 

2019 def illumination_intensity(self) -> float: 

2020 """Illumination intensity.""" 

2021 return float(self._com.IlluminationIntensity) 

2022 

2023 @property 

2024 def illumination_state(self) -> Any: 

2025 """Illumination state.""" 

2026 return self._com.IlluminationState 

2027 

2028 @property 

2029 def key(self) -> Any: 

2030 """Sensor-head identifier key.""" 

2031 return self._com.Key 

2032 

2033 @property 

2034 def name(self) -> str: 

2035 """Sensor-head name.""" 

2036 return self._com.Name 

2037 

2038 @property 

2039 def sensor_head_info(self) -> SensorHeadInfo: 

2040 """Information object for the sensor head.""" 

2041 return SensorHeadInfo(self._com.SensorHeadInfo) 

2042 

2043 @property 

2044 def video_camera_settings(self) -> VideoCameraSettings: 

2045 """Video-camera settings for the sensor head. 

2046 

2047 Raises 

2048 ------ 

2049 ValueError 

2050 If no video-camera settings are available. 

2051 """ 

2052 if self._com.VideoCameraSettings is None: 

2053 raise ValueError(f"No video camera settings found for {self!r}") 

2054 return VideoCameraSettings(self._com.VideoCameraSettings) 

2055 

2056 

2057class FilterSettings: 

2058 """Wrapper around COM controller-filter settings.""" 

2059 

2060 def __init__(self, obj: Any) -> None: 

2061 self._com = obj 

2062 

2063 @property 

2064 def key(self) -> FilterType: 

2065 """Filter type key.""" 

2066 return FilterType(self._com.Key) 

2067 

2068 @property 

2069 def name(self) -> str: 

2070 """Filter name.""" 

2071 return self._com.Name 

2072 

2073 @property 

2074 def range(self) -> Any: 

2075 """Filter range setting.""" 

2076 return self._com.Range 

2077 

2078 

2079class DecoderInfo: 

2080 """Wrapper around COM decoder information.""" 

2081 

2082 def __init__(self, obj: Any) -> None: 

2083 self._com = obj 

2084 

2085 @property 

2086 def firmware_version(self) -> str: 

2087 """Decoder firmware version.""" 

2088 return self._com.FirmwareVersion 

2089 

2090 @property 

2091 def name(self) -> str: 

2092 """Decoder name.""" 

2093 return self._com.Name 

2094 

2095 

2096class QuantitySettings: 

2097 """Wrapper around COM quantity settings.""" 

2098 

2099 def __init__(self, obj: Any) -> None: 

2100 self._com = obj 

2101 

2102 @property 

2103 def bandwidth_range(self) -> str: 

2104 """Active decoder bandwidth range name.""" 

2105 return self._com.BandwidthRange 

2106 

2107 @property 

2108 def decoder_info(self) -> DecoderInfo: 

2109 """Decoder information for this quantity.""" 

2110 return DecoderInfo(self._com.DecoderInfo) 

2111 

2112 @property 

2113 def key(self) -> QuantityType: 

2114 """Quantity-type key.""" 

2115 return QuantityType(self._com.Key) 

2116 

2117 @property 

2118 def max_velocity_range(self) -> float: 

2119 """Maximum velocity range.""" 

2120 return float(self._com.MaxVelocityRange) 

2121 

2122 @property 

2123 def name(self) -> str: 

2124 """Quantity name.""" 

2125 return self._com.Name 

2126 

2127 @property 

2128 def output_active(self) -> bool: 

2129 """Whether output of the quantity is active.""" 

2130 return bool(self._com.OutputActive) 

2131 

2132 @property 

2133 def overrun(self) -> Any: 

2134 """Overrun mode/state.""" 

2135 return self._com.Overrun 

2136 

2137 @property 

2138 def range(self) -> str: 

2139 """Active decoder range name.""" 

2140 return self._com.Range 

2141 

2142 

2143class ControllerSettings: 

2144 """Wrapper around COM vibrometer-controller settings.""" 

2145 

2146 def __init__(self, obj: Any) -> None: 

2147 self._com = obj 

2148 

2149 @property 

2150 def controller_info(self) -> ControllerInfo: 

2151 """Controller information.""" 

2152 return ControllerInfo(self._com.ControllerInfo) 

2153 

2154 @property 

2155 def decoder_clear_mode(self) -> Any: 

2156 """Decoder clear mode.""" 

2157 return self._com.DecoderClearMode 

2158 

2159 @property 

2160 def filter_settings(self) -> list[FilterSettings]: 

2161 """Filter settings.""" 

2162 return [FilterSettings(x) for x in self._com.FilterSettingsCollection] 

2163 

2164 @property 

2165 def quantity_settings(self) -> list[QuantitySettings]: 

2166 """Quantity settings.""" 

2167 return [QuantitySettings(x) for x in self._com.QuantitySettingsCollection] 

2168 

2169 @property 

2170 def sensor_head_settings(self) -> list[SensorHeadSettings]: 

2171 """Sensor-head settings.""" 

2172 return [SensorHeadSettings(x) for x in self._com.SensorHeadSettingsCollection] 

2173 

2174 

2175class VibrometerProperties: 

2176 """Wrapper around COM vibrometer acquisition properties.""" 

2177 

2178 def __init__(self, obj: Any) -> None: 

2179 self._com = obj 

2180 

2181 @property 

2182 def controller_settings(self) -> ControllerSettings: 

2183 """Vibrometer controller settings.""" 

2184 return ControllerSettings(self._com.VibControllerSettings) 

2185 

2186 

2187class ZoomFFTProperties: 

2188 """Wrapper around COM zoom-FFT acquisition properties.""" 

2189 

2190 def __init__(self, obj: Any) -> None: 

2191 self._com = obj 

2192 

2193 @property 

2194 def bandwidth(self) -> float: 

2195 """Zoom-FFT bandwidth in hertz.""" 

2196 return float(self._com.Bandwidth) 

2197 

2198 @property 

2199 def center_frequency(self) -> float: 

2200 """Zoom-FFT center frequency in hertz.""" 

2201 return float(self._com.CenterFrequency) 

2202 

2203 @property 

2204 def lines(self) -> int: 

2205 """Number of Zoom-FFT lines.""" 

2206 return int(self._com.Lines) 

2207 

2208 @property 

2209 def overlap(self) -> float: 

2210 """Zoom-FFT overlap in percent.""" 

2211 return float(self._com.Overlap) 

2212 

2213 @property 

2214 def sample_frequency(self) -> float: 

2215 """Sample frequency in hertz.""" 

2216 return float(self._com.SampleFrequency) 

2217 

2218 @property 

2219 def sample_resolution(self) -> float: 

2220 """Frequency resolution in hertz.""" 

2221 return float(self._com.SampleResolution) 

2222 

2223 @property 

2224 def samples(self) -> int: 

2225 """Number of samples used for the Zoom-FFT.""" 

2226 return int(self._com.Samples) 

2227 

2228 @property 

2229 def sample_time(self) -> float: 

2230 """Sample time in seconds.""" 

2231 return float(self._com.SampleTime) 

2232 

2233 

2234# Alignments 

2235# Alignments 

2236 

2237 

2238class AlignmentPoint2D: 

2239 """Wrapper around a COM 2D alignment point.""" 

2240 

2241 def __init__(self, obj: Any) -> None: 

2242 self._com = obj 

2243 

2244 @property 

2245 def scanner_x(self) -> float: 

2246 """Horizontal scanner value of the 2D alignment point.""" 

2247 return float(self._com.ScannerX) 

2248 

2249 @property 

2250 def scanner_y(self) -> float: 

2251 """Vertical scanner value of the 2D alignment point.""" 

2252 return float(self._com.ScannerY) 

2253 

2254 @property 

2255 def video_x(self) -> float: 

2256 """X video coordinate of the 2D alignment point.""" 

2257 return float(self._com.VideoX) 

2258 

2259 @property 

2260 def video_y(self) -> float: 

2261 """Y video coordinate of the 2D alignment point.""" 

2262 return float(self._com.VideoY) 

2263 

2264 

2265class Alignment2D: 

2266 """Wrapper around COM 2D alignment data and operations.""" 

2267 

2268 def __init__(self, obj: Any) -> None: 

2269 self._com = obj 

2270 

2271 @property 

2272 def align_2d_points(self) -> list[AlignmentPoint2D]: 

2273 """2D alignment points.""" 

2274 return [AlignmentPoint2D(p) for p in self._com.Align2DPoints] 

2275 

2276 @property 

2277 def scan_head_distance(self) -> float: 

2278 """Distance from the scan head front panel to the object, in meters.""" 

2279 return float(self._com.ScanHeadDistance) 

2280 

2281 @property 

2282 def scan_head_type(self) -> ScanHeadType: 

2283 """Type of scanning head used for the alignment.""" 

2284 return ScanHeadType(self._com.ScanHeadType) 

2285 

2286 @property 

2287 def scanner_quantity(self) -> Any: 

2288 """Physical quantity represented by the scanner coordinates.""" 

2289 return self._com.ScannerQuantity 

2290 

2291 @property 

2292 def valid(self) -> bool: 

2293 """Whether the 2D alignment is valid.""" 

2294 return bool(self._com.Valid) 

2295 

2296 def calculate(self) -> None: 

2297 """Calculate a new 2D alignment.""" 

2298 self._com.Calculate() 

2299 

2300 def get_cosine_correction_factor( 

2301 self, 

2302 scanner_x: float, 

2303 scanner_y: float, 

2304 vib_correct_x: bool, 

2305 vib_correct_y: bool, 

2306 ) -> float: 

2307 """Return the cosine-correction factor for the given scanner coordinates.""" 

2308 return float( 

2309 self._com.GetCosineCorrectionFactor(scanner_x, scanner_y, vib_correct_x, vib_correct_y) 

2310 ) 

2311 

2312 def invalidate(self) -> None: 

2313 """Invalidate the current 2D alignment and reset related properties.""" 

2314 self._com.Invalidate() 

2315 

2316 def scanner_to_video(self, scanner_x: float, scanner_y: float) -> Any: 

2317 """Convert scanner coordinates to video coordinates.""" 

2318 return self._com.ScannerToVideo(scanner_x, scanner_y) 

2319 

2320 def video_to_scanner(self, video_x: float, video_y: float) -> Any: 

2321 """Convert video coordinates to scanner coordinates.""" 

2322 return self._com.VideoToScanner(video_x, video_y) 

2323 

2324 

2325class AlignmentPoint3D: 

2326 """Wrapper around a COM 3D alignment point.""" 

2327 

2328 def __init__(self, obj: Any) -> None: 

2329 self._com = obj 

2330 

2331 @property 

2332 def caps_value(self) -> int: 

2333 """Capabilities of the 3D alignment point as a raw bitmask.""" 

2334 return int(self._com.Caps) 

2335 

2336 @property 

2337 def caps(self) -> AlignmentPoint3DCaps: 

2338 """Capabilities of the 3D alignment point as an ``IntFlag``.""" 

2339 return AlignmentPoint3DCaps(self.caps_value) 

2340 

2341 @property 

2342 def distance(self) -> float: 

2343 """Distance associated with the alignment point.""" 

2344 return float(self._com.Distance) 

2345 

2346 @property 

2347 def label(self) -> str: 

2348 """Alignment-point label.""" 

2349 return self._com.Label 

2350 

2351 @property 

2352 def point_type(self) -> AlignmentPoint3DType: 

2353 """Alignment-point type.""" 

2354 return AlignmentPoint3DType(self._com.PointType) 

2355 

2356 @property 

2357 def quality(self) -> float: 

2358 """Alignment quality in meters.""" 

2359 return float(self._com.Quality) 

2360 

2361 @property 

2362 def scanner_x(self) -> float: 

2363 """Horizontal scanner value of the 3D alignment point.""" 

2364 return float(self._com.ScannerX) 

2365 

2366 @property 

2367 def scanner_y(self) -> float: 

2368 """Vertical scanner value of the 3D alignment point.""" 

2369 return float(self._com.ScannerY) 

2370 

2371 @property 

2372 def video_x(self) -> float: 

2373 """X video coordinate of the 3D alignment point.""" 

2374 return float(self._com.VideoX) 

2375 

2376 @property 

2377 def video_y(self) -> float: 

2378 """Y video coordinate of the 3D alignment point.""" 

2379 return float(self._com.VideoY) 

2380 

2381 @property 

2382 def x(self) -> float: 

2383 """X coordinate of the 3D alignment point.""" 

2384 return float(self._com.X) 

2385 

2386 @property 

2387 def y(self) -> float: 

2388 """Y coordinate of the 3D alignment point.""" 

2389 return float(self._com.Y) 

2390 

2391 @property 

2392 def z(self) -> float: 

2393 """Z coordinate of the 3D alignment point.""" 

2394 return float(self._com.Z) 

2395 

2396 

2397class Alignment3D: 

2398 """Wrapper around COM 3D alignment data and operations.""" 

2399 

2400 def __init__(self, obj: Any) -> None: 

2401 self._com = obj 

2402 

2403 @property 

2404 def align_3d_points(self) -> list[AlignmentPoint3D]: 

2405 """3D alignment points.""" 

2406 return [AlignmentPoint3D(p) for p in self._com.Align3DPoints] 

2407 

2408 @property 

2409 def coord_definition_mode(self) -> CoordDefinitionMode: 

2410 """Coordinate-definition mode for the 3D alignment.""" 

2411 return CoordDefinitionMode(self._com.CoordDefinitionMode) 

2412 

2413 @property 

2414 def current_quality(self) -> float: 

2415 """Current 3D alignment quality in meters.""" 

2416 return float(self._com.CurrentQuality) 

2417 

2418 @property 

2419 def mirror(self) -> bool: 

2420 """Whether the 3D alignment was performed through a mirror.""" 

2421 return bool(self._com.Mirror) 

2422 

2423 @property 

2424 def scan_head_type(self) -> ScanHeadType: 

2425 """Scanning-head type.""" 

2426 return ScanHeadType(self._com.ScanHeadType) 

2427 

2428 @property 

2429 def target_quality(self) -> float: 

2430 """Target 3D alignment quality in meters.""" 

2431 return float(self._com.TargetQuality) 

2432 

2433 @property 

2434 def valid(self) -> bool: 

2435 """Whether the alignment is valid.""" 

2436 return bool(self._com.Valid) 

2437 

2438 def calculate(self) -> None: 

2439 """Calculate a new 3D alignment.""" 

2440 self._com.Calculate() 

2441 

2442 def coord_3d_to_scanner(self, x: float, y: float, z: float) -> Any: 

2443 """Convert 3D coordinates to scanner coordinates and distance.""" 

2444 return self._com.Coord3DToScanner(x, y, z) 

2445 

2446 def get_beam_length(self, x: float, y: float, z: float) -> float: 

2447 """Retrieve beam length from the objective to the specified point.""" 

2448 return float(self._com.GetBeamLength(x, y, z)) 

2449 

2450 def get_beam_origin(self, scanner_x: float = 0.0, scanner_y: float = 0.0) -> Any: 

2451 """Retrieve the beam origin at the second scanner mirror.""" 

2452 return self._com.GetBeamOrigin(scanner_x, scanner_y) 

2453 

2454 def get_scan_head_position(self) -> Any: 

2455 """Retrieve the scan-head position at the first scanner mirror.""" 

2456 return self._com.GetScanHeadPosition() 

2457 

2458 def invalidate(self) -> None: 

2459 """Invalidate the current 3D alignment.""" 

2460 self._com.Invalidate() 

2461 

2462 def scanner_to_coord_3d(self, scanner_x: float, scanner_y: float, distance: float) -> Any: 

2463 """Convert scanner coordinates and distance to 3D coordinates.""" 

2464 return self._com.ScannerToCoord3D(scanner_x, scanner_y, distance) 

2465 

2466 def scanner_to_vector_3d(self, scanner_x: float, scanner_y: float) -> Any: 

2467 """Convert scanner coordinates to a unit-direction vector.""" 

2468 return self._com.ScannerToVector3D(scanner_x, scanner_y) 

2469 

2470 

2471class AlignmentCamera: 

2472 """Wrapper around COM camera-alignment data and operations.""" 

2473 

2474 def __init__(self, obj: Any) -> None: 

2475 self._com = obj 

2476 

2477 @property 

2478 def align_3d_points(self) -> list[AlignmentPoint3D]: 

2479 """3D alignment points.""" 

2480 return [AlignmentPoint3D(p) for p in self._com.Align3DPoints] 

2481 

2482 @property 

2483 def aspect_ratio(self) -> float: 

2484 """Video width-to-height aspect ratio.""" 

2485 return float(self._com.AspectRatio) 

2486 

2487 @property 

2488 def cam_pos_x(self) -> float: 

2489 """X coordinate of the camera position.""" 

2490 return float(self._com.CamPosX) 

2491 

2492 @property 

2493 def cam_pos_y(self) -> float: 

2494 """Y coordinate of the camera position.""" 

2495 return float(self._com.CamPosY) 

2496 

2497 @property 

2498 def cam_pos_z(self) -> float: 

2499 """Z coordinate of the camera position.""" 

2500 return float(self._com.CamPosZ) 

2501 

2502 @property 

2503 def cam_vec_x(self) -> float: 

2504 """X coordinate of the camera view direction.""" 

2505 return float(self._com.CamVecX) 

2506 

2507 @property 

2508 def cam_vec_y(self) -> float: 

2509 """Y coordinate of the camera view direction.""" 

2510 return float(self._com.CamVecY) 

2511 

2512 @property 

2513 def cam_vec_z(self) -> float: 

2514 """Z coordinate of the camera view direction.""" 

2515 return float(self._com.CamVecZ) 

2516 

2517 @property 

2518 def current_quality(self) -> float: 

2519 """Current alignment quality.""" 

2520 return float(self._com.CurrentQuality) 

2521 

2522 @property 

2523 def mirror(self) -> bool: 

2524 """Whether the object is viewed through a mirror.""" 

2525 return bool(self._com.Mirror) 

2526 

2527 @property 

2528 def target_quality(self) -> float: 

2529 """Target alignment quality.""" 

2530 return float(self._com.TargetQuality) 

2531 

2532 @property 

2533 def valid(self) -> bool: 

2534 """Whether the current alignment is valid.""" 

2535 return bool(self._com.Valid) 

2536 

2537 @property 

2538 def view_angle(self) -> float: 

2539 """Camera aperture angle in degrees.""" 

2540 return float(self._com.ViewAngle) 

2541 

2542 @property 

2543 def view_up_x(self) -> float: 

2544 """X coordinate of the camera view-up direction.""" 

2545 return float(self._com.ViewUpX) 

2546 

2547 @property 

2548 def view_up_y(self) -> float: 

2549 """Y coordinate of the camera view-up direction.""" 

2550 return float(self._com.ViewUpY) 

2551 

2552 @property 

2553 def view_up_z(self) -> float: 

2554 """Z coordinate of the camera view-up direction.""" 

2555 return float(self._com.ViewUpZ) 

2556 

2557 def calculate(self, *args: Any) -> Any: 

2558 """Calculate camera parameters from input parameters and alignment points.""" 

2559 return self._com.Calculate(*args) 

2560 

2561 def choose_camera_position(self, *args: Any) -> Any: 

2562 """Choose a camera position to view the specified 3D points.""" 

2563 return self._com.ChooseCameraPosition(*args) 

2564 

2565 def coord_3d_to_video(self, x: float, y: float, z: float) -> Any: 

2566 """Convert 3D object coordinates to video coordinates.""" 

2567 return self._com.Coord3DToVideo(x, y, z) 

2568 

2569 def scanner_to_video(self, *args: Any) -> Any: 

2570 """Convert scanner coordinates to video coordinates.""" 

2571 return self._com.ScannerToVideo(*args) 

2572 

2573 def set_camera(self, *args: Any) -> Any: 

2574 """Set camera parameters.""" 

2575 return self._com.SetCamera(*args) 

2576 

2577 def video_to_coord_3d(self, *args: Any) -> Any: 

2578 """Convert video coordinates to 3D object coordinates.""" 

2579 return self._com.VideoToCoord3D(*args) 

2580 

2581 def video_to_scanner(self, *args: Any) -> Any: 

2582 """Convert video coordinates to scanner coordinates.""" 

2583 return self._com.VideoToScanner(*args) 

2584 

2585 

2586class Alignments: 

2587 """Wrapper around the COM alignments info object.""" 

2588 

2589 def __init__(self, obj: Any) -> None: 

2590 self._com = obj 

2591 

2592 @property 

2593 def alignments_2d(self) -> list[Alignment2D]: 

2594 """2D alignments.""" 

2595 return [Alignment2D(a) for a in self._com.Alignments2D] 

2596 

2597 @property 

2598 def alignments_3d(self) -> list[Alignment3D]: 

2599 """3D alignments.""" 

2600 return [Alignment3D(a) for a in self._com.Alignments3D] 

2601 

2602 @property 

2603 def alignments_camera(self) -> list[AlignmentCamera]: 

2604 """Camera alignments.""" 

2605 return [AlignmentCamera(a) for a in self._com.AlignmentsCamera] 

2606 

2607 @property 

2608 def name(self) -> str: 

2609 """Info-object name.""" 

2610 return self._com.Name 

2611 

2612 @property 

2613 def type(self) -> Any: 

2614 """Info-object type identifier.""" 

2615 return self._com.Type 

2616 

2617 def get_video_rect(self) -> tuple[float, float, float, float]: 

2618 """Gets the video rectangle defining the video coordinate system. 

2619 

2620 Returns 

2621 ------- 

2622 left : float 

2623 The left side of the video rectangle 

2624 top : float 

2625 The top side of the video rectangle 

2626 right : float 

2627 The right side of the video rectangle 

2628 bottom : float 

2629 The bottom side of the video rectangle 

2630 """ 

2631 return self._com.GetVideoRect() 

2632 

2633 

2634class Element: 

2635 """Wrapper around a COM geometry element.""" 

2636 

2637 def __init__(self, obj: Any) -> None: 

2638 self._com = obj 

2639 

2640 @property 

2641 def meas_point_indices(self) -> list[int]: 

2642 """Measurement-point indices used by the element.""" 

2643 return [int(v) for v in self._com.MeasPointIndices] 

2644 

2645 @property 

2646 def meas_point_labels(self) -> list[Any]: 

2647 """Measurement-point labels used by the element.""" 

2648 return list(self._com.MeasPointLabels) 

2649 

2650 def meas_point_indices_by_status( 

2651 self, 

2652 status_mask: ScanStatus, 

2653 status_compare: ScanStatus, 

2654 any_bit: bool, 

2655 ) -> list[int]: 

2656 """Return measurement-point indices matching a scan-status mask.""" 

2657 return [ 

2658 int(v) 

2659 for v in self._com.MeasPointIndicesByStatus( 

2660 int(status_mask), int(status_compare), any_bit 

2661 ) 

2662 ] 

2663 

2664 def meas_point_labels_by_status( 

2665 self, 

2666 status_mask: ScanStatus, 

2667 status_compare: ScanStatus, 

2668 any_bit: bool, 

2669 ) -> list[Any]: 

2670 """Return measurement-point labels matching a scan-status mask.""" 

2671 return list( 

2672 self._com.MeasPointLabelsByStatus(int(status_mask), int(status_compare), any_bit) 

2673 ) 

2674 

2675 

2676class FrontEnd: 

2677 """Wrapper around COM front-end information.""" 

2678 

2679 def __init__(self, obj: Any, *, expose_internals: bool = True) -> None: 

2680 self._com = obj 

2681 self._expose_internals = expose_internals 

2682 

2683 @property 

2684 def caps_value(self) -> int: 

2685 """Front-end capabilities as a raw bitmask.""" 

2686 return int(self._com.Caps) 

2687 

2688 @property 

2689 def caps(self) -> FrontEndCaps: 

2690 """Front-end capabilities as a decoded flag set.""" 

2691 return FrontEndCaps(self.caps_value) 

2692 

2693 @property 

2694 def firmware_version(self) -> str: 

2695 """Firmware version of the front-end.""" 

2696 return self._com.FirmwareVersion 

2697 

2698 @property 

2699 def host_ip_address(self) -> str: 

2700 """IP address of the connected host network adapter.""" 

2701 return self._com.HostIpAddress 

2702 

2703 @property 

2704 def mac_address(self) -> str: 

2705 """MAC address of the front-end.""" 

2706 return self._com.MacAddress 

2707 

2708 @property 

2709 def name(self) -> str: 

2710 """Front-end name.""" 

2711 return self._com.Name 

2712 

2713 @property 

2714 def type(self) -> FrontEndType: 

2715 """Front-end type.""" 

2716 return FrontEndType(self._com.Type) 

2717 

2718 @property 

2719 def front_ends(self) -> Any: 

2720 """Parent raw COM front-end collection.""" 

2721 return self._com.FrontEnds 

2722 

2723 @property 

2724 def control(self) -> Any: 

2725 """Undocumented internal COM control object.""" 

2726 if not self._expose_internals: 

2727 raise AttributeError( 

2728 "FrontEnd.Control is undocumented; pass expose_internals=True " "to access it." 

2729 ) 

2730 return self._com.Control 

2731 

2732 @property 

2733 def logical_channels(self) -> Any: 

2734 """Undocumented internal COM logical-channel collection.""" 

2735 if not self._expose_internals: 

2736 raise AttributeError( 

2737 "FrontEnd.LogicalChannels is undocumented; pass " 

2738 "expose_internals=True to access it." 

2739 ) 

2740 return self._com.LogicalChannels 

2741 

2742 @property 

2743 def physical_channels(self) -> Any: 

2744 """Undocumented internal COM physical-channel collection.""" 

2745 if not self._expose_internals: 

2746 raise AttributeError( 

2747 "FrontEnd.PhysicalChannels is undocumented; pass " 

2748 "expose_internals=True to access it." 

2749 ) 

2750 return self._com.PhysicalChannels 

2751 

2752 

2753class Hardware: 

2754 """Wrapper around COM hardware info.""" 

2755 

2756 def __init__(self, obj: Any) -> None: 

2757 self._com = obj 

2758 

2759 @property 

2760 def acq_board(self) -> AcqBoard: 

2761 """Acquisition-board type used for the measurement.""" 

2762 return AcqBoard(self._com.AcqBoard) 

2763 

2764 @property 

2765 def acq_board_channel_count(self) -> int: 

2766 """Number of acquisition-board channels.""" 

2767 return int(self._com.AcqBoardChannelCount) 

2768 

2769 @property 

2770 def acq_board_firmware_version(self) -> str: 

2771 """Acquisition-board firmware version.""" 

2772 return self._com.AcqBoardFirmwareVersion 

2773 

2774 @property 

2775 def acq_board_max_frequency(self) -> float: 

2776 """Maximum acquisition-board frequency.""" 

2777 return float(self._com.AcqBoardMaxFrequency) 

2778 

2779 @property 

2780 def acq_board_name(self) -> str: 

2781 """Acquisition-board name.""" 

2782 return self._com.AcqBoardName 

2783 

2784 @property 

2785 def active_front_end(self) -> FrontEnd: 

2786 """Active front-end used for the measurement.""" 

2787 return FrontEnd(self._com.ActiveFrontEnd) 

2788 

2789 @property 

2790 def coherence_optimizer_versions(self) -> list[str]: 

2791 """Versions of system coherence optimizers.""" 

2792 return list(self._com.CoherenceOptimizerVersions) 

2793 

2794 @property 

2795 def front_end(self) -> FrontEndType: 

2796 """Front-end type used for the measurement.""" 

2797 return FrontEndType(self._com.FrontEnd) 

2798 

2799 @property 

2800 def front_end_firmware_version(self) -> str: 

2801 """Front-end firmware version.""" 

2802 return self._com.FrontEndFirmwareVersion 

2803 

2804 @property 

2805 def generator_channel_count(self) -> int: 

2806 """Number of generator channels.""" 

2807 return int(self._com.GeneratorChannelCount) 

2808 

2809 @property 

2810 def generator_firmware_version(self) -> str: 

2811 """Generator firmware version.""" 

2812 return self._com.GeneratorFirmwareVersion 

2813 

2814 @property 

2815 def generator_name(self) -> str: 

2816 """Generator name.""" 

2817 return self._com.GeneratorName 

2818 

2819 @property 

2820 def generator_type(self) -> GeneratorType: 

2821 """Generator type.""" 

2822 return GeneratorType(self._com.GeneratorType) 

2823 

2824 @property 

2825 def name(self) -> str: 

2826 """Info-object name.""" 

2827 return self._com.Name 

2828 

2829 @property 

2830 def scan_head_type(self) -> ScanHeadType: 

2831 """Scanning-head type used for the measurement.""" 

2832 return ScanHeadType(self._com.ScanHeadType) 

2833 

2834 @property 

2835 def sensor_head_names(self) -> list[str]: 

2836 """Names of sensor heads used for the measurement.""" 

2837 return list(self._com.SensorHeadNames) 

2838 

2839 @property 

2840 def sensor_head_versions(self) -> list[str]: 

2841 """Versions of sensor heads used for the measurement.""" 

2842 return list(self._com.SensorHeadVersions) 

2843 

2844 @property 

2845 def teds_sensors(self) -> Any: 

2846 """TEDS sensor information.""" 

2847 return self._com.TedsSensors 

2848 

2849 @property 

2850 def type(self) -> Any: 

2851 """Info-object type identifier.""" 

2852 return self._com.Type 

2853 

2854 

2855class GeometryComponent: 

2856 """Wrapper around a COM geometry component.""" 

2857 

2858 def __init__(self, obj: Any) -> None: 

2859 self._com = obj 

2860 

2861 @property 

2862 def components(self) -> list["GeometryComponent"]: 

2863 """Child geometry components.""" 

2864 return [GeometryComponent(c) for c in self._com.Components] 

2865 

2866 @property 

2867 def description(self) -> str: 

2868 """Geometry-component description.""" 

2869 return self._com.Description 

2870 

2871 @property 

2872 def id(self) -> Any: 

2873 """Unique identifier of the geometry component.""" 

2874 return self._com.ID 

2875 

2876 @property 

2877 def indices(self) -> list[int]: 

2878 """Measurement-point indices attached to the component.""" 

2879 return [int(v) for v in self._com.Indices] 

2880 

2881 @property 

2882 def labels(self) -> list[Any]: 

2883 """Measurement-point labels attached to the component.""" 

2884 return list(self._com.Labels) 

2885 

2886 @property 

2887 def name(self) -> str: 

2888 """Geometry-component name.""" 

2889 return self._com.Name 

2890 

2891 

2892class MeasPoint: 

2893 """Wrapper around a COM measurement point.""" 

2894 

2895 def __init__(self, obj: Any) -> None: 

2896 self._com = obj 

2897 

2898 def coord_xyz(self) -> Any: 

2899 """Retrieve the 3D coordinate of the measurement point.""" 

2900 return self._com.CoordXYZ(0, 0, 0) 

2901 

2902 def set_coord_xyz(self, x: float, y: float, z: float) -> None: 

2903 """Set the 3D coordinate of the measurement point.""" 

2904 self._com.SetCoordXYZ(x, y, z) 

2905 

2906 def set_texture_xy_index(self, tex_x: float, tex_y: float, tex_index: int) -> None: 

2907 """Set texture coordinates and texture index for the point.""" 

2908 self._com.SetTextureXYIndex(tex_x, tex_y, tex_index) 

2909 

2910 def set_video_xy(self, video_x: float, video_y: float) -> None: 

2911 """Set the video coordinate of the measurement point.""" 

2912 self._com.SetVideoXY(video_x, video_y) 

2913 

2914 def texture_xy_index(self) -> Any: 

2915 """Get texture coordinates and texture index for the point.""" 

2916 return self._com.TextureXYIndex(0, 0, 0) 

2917 

2918 def toggle_valid_invalidated(self) -> None: 

2919 """Toggle the invalidated state of the measurement point.""" 

2920 self._com.ToggleValidInvalidated() 

2921 

2922 def video_xy(self) -> Any: 

2923 """Retrieve the video coordinate of the measurement point.""" 

2924 return self._com.VideoXY(0, 0) 

2925 

2926 @property 

2927 def average_count(self) -> int: 

2928 """Average count for the measurement point.""" 

2929 return int(self._com.AverageCount) 

2930 

2931 @property 

2932 def component(self) -> GeometryComponent: 

2933 """Geometry component to which this point belongs.""" 

2934 return GeometryComponent(self._com.Component) 

2935 

2936 @property 

2937 def contained_file_active_id(self) -> Any: 

2938 """Contained-file active identifier for combined files.""" 

2939 return self._com.ContainedFileActiveID 

2940 

2941 @property 

2942 def contained_file_active_mp_index(self) -> int: 

2943 """Active contained-file measurement-point index.""" 

2944 return int(self._com.ContainedFileActiveMPIndex) 

2945 

2946 @property 

2947 def contained_file_index(self) -> int: 

2948 """Contained-file index for combined files.""" 

2949 return int(self._com.ContainedFileIndex) 

2950 

2951 @property 

2952 def contained_file_meas_point_index(self) -> int: 

2953 """Contained-file measurement-point index.""" 

2954 return int(self._com.ContainedFileMeasPointIndex) 

2955 

2956 @property 

2957 def focus_status(self) -> FocusStatus: 

2958 """Laser-focus status flags.""" 

2959 return FocusStatus(int(self._com.FocusStatus)) 

2960 

2961 @property 

2962 def focus_values(self) -> Any: 

2963 """Laser-focus values.""" 

2964 return self._com.FocusValues 

2965 

2966 @property 

2967 def geometry_status(self) -> GeometryStatus: 

2968 """3D geometry status flags.""" 

2969 return GeometryStatus(int(self._com.GeometryStatus)) 

2970 

2971 @property 

2972 def index(self) -> int: 

2973 """Measurement-point index.""" 

2974 return int(self._com.Index) 

2975 

2976 @property 

2977 def label(self) -> Any: 

2978 """Measurement-point label.""" 

2979 return self._com.Label 

2980 

2981 @property 

2982 def min_sigma(self) -> float: 

2983 """Minimum signal-enhancement sigma.""" 

2984 return float(self._com.MinSigma) 

2985 

2986 @property 

2987 def path_length_status(self) -> Any: 

2988 """Optical path-length status.""" 

2989 return self._com.PathLengthStatus 

2990 

2991 @property 

2992 def path_length_value(self) -> Any: 

2993 """Optical path-length value.""" 

2994 return self._com.PathLengthValue 

2995 

2996 @property 

2997 def scan_distances(self) -> list[float]: 

2998 """Distances from the scanning head to the measurement point.""" 

2999 return [float(v) for v in self._com.ScanDistances] 

3000 

3001 @property 

3002 def scan_quantities_x(self) -> list[float]: 

3003 """Scanner X values of the measurement point.""" 

3004 return [float(v) for v in self._com.ScanQuantitiesX] 

3005 

3006 @property 

3007 def scan_quantities_y(self) -> list[float]: 

3008 """Scanner Y values of the measurement point.""" 

3009 return [float(v) for v in self._com.ScanQuantitiesY] 

3010 

3011 @property 

3012 def scan_quantity(self) -> PhysicalQuantity: 

3013 """Physical quantity of the scanner values.""" 

3014 return PhysicalQuantity(self._com.ScanQuantity) 

3015 

3016 @property 

3017 def scan_status(self) -> ScanStatus: 

3018 """Measurement status flags.""" 

3019 return ScanStatus(int(self._com.ScanStatus)) 

3020 

3021 @property 

3022 def scan_status_disabled(self) -> bool: 

3023 """Whether the scan-status disabled flag is set.""" 

3024 return bool(self._com.ScanStatusDisabled) 

3025 

3026 @property 

3027 def sigma(self) -> float: 

3028 """Signal-enhancement sigma.""" 

3029 return float(self._com.Sigma) 

3030 

3031 @property 

3032 def vibrometer_range(self) -> Any: 

3033 """Vibrometer range used for the point.""" 

3034 return self._com.VibrometerRange 

3035 

3036 

3037class MeasPoints: 

3038 """Pythonic wrapper around the COM measurement-point collection.""" 

3039 

3040 def __init__(self, obj: Any) -> None: 

3041 self._com = obj 

3042 

3043 def __len__(self) -> int: 

3044 """Number of measurement points.""" 

3045 return int(self._com.Count) 

3046 

3047 def __getitem__(self, index: int) -> MeasPoint: 

3048 """Return a measurement point by zero-based Python index.""" 

3049 count = len(self) 

3050 if index < 0: 

3051 index += count 

3052 if index < 0 or index >= count: 

3053 raise IndexError("MeasPoints index out of range") 

3054 return MeasPoint(self._com.Item(index + 1)) 

3055 

3056 def __iter__(self): 

3057 """Iterate over measurement points.""" 

3058 for i in range(len(self)): 

3059 yield self[i] 

3060 

3061 @property 

3062 def average_sigma(self) -> float: 

3063 """Average signal-enhancement sigma for all points.""" 

3064 return float(self._com.AverageSigma) 

3065 

3066 @property 

3067 def component(self) -> GeometryComponent: 

3068 """Root geometry component.""" 

3069 return GeometryComponent(self._com.Component) 

3070 

3071 @property 

3072 def coordinates_to_array(self) -> np.ndarray: 

3073 """3D coordinates for all points as an array of shape ``(n, 3)``.""" 

3074 return np.asarray(self._com.CoordinatesToArray).reshape((-1, 3)) 

3075 

3076 @property 

3077 def count(self) -> int: 

3078 """Number of measurement points.""" 

3079 return int(self._com.Count) 

3080 

3081 @property 

3082 def has_3d_coordinates(self) -> bool: 

3083 """Whether the points are defined with 3D coordinates.""" 

3084 return bool(self._com.Has3DCoordinates) 

3085 

3086 @property 

3087 def has_non_optimal_points_to_invalidate(self) -> bool: 

3088 """Whether non-optimal points exist that can be invalidated.""" 

3089 return bool(self._com.HasNonOptimalPointsToInvalidate) 

3090 

3091 @property 

3092 def has_sigma(self) -> bool: 

3093 """Whether average signal-enhancement sigma is valid.""" 

3094 return bool(self._com.HasSigma) 

3095 

3096 @property 

3097 def is_3d_calculation_possible(self) -> bool: 

3098 """Whether 3D coordinate calculation is possible.""" 

3099 return bool(self._com.Is3DCalculationPossible) 

3100 

3101 @property 

3102 def name(self) -> str: 

3103 """Info-object name.""" 

3104 return self._com.Name 

3105 

3106 @property 

3107 def type(self) -> Any: 

3108 """Info-object type identifier.""" 

3109 return self._com.Type 

3110 

3111 @property 

3112 def video_coordinates_to_array(self) -> np.ndarray: 

3113 """Video coordinates for all points as an array of shape ``(n, 2)``.""" 

3114 return np.asarray(self._com.VideoCoordinatesToArray).reshape((-1, 2)) 

3115 

3116 def get_video_rect(self) -> tuple[float, float, float, float]: 

3117 """Gets the video rectangle defining the video coordinate system. 

3118 

3119 Returns 

3120 ------- 

3121 left : float 

3122 The left side of the video rectangle 

3123 top : float 

3124 The top side of the video rectangle 

3125 right : float 

3126 The right side of the video rectangle 

3127 bottom : float 

3128 The bottom side of the video rectangle 

3129 """ 

3130 return self._com.GetVideoRect() 

3131 

3132 

3133class MeasurementLocation: 

3134 """Wrapper around a COM measurement-location object.""" 

3135 

3136 def __init__(self, obj: Any) -> None: 

3137 self._com = obj 

3138 

3139 @property 

3140 def channel_name(self) -> str: 

3141 """Channel name.""" 

3142 return self._com.ChannelName 

3143 

3144 @property 

3145 def direction(self) -> Any: 

3146 """Measurement direction or directions in object coordinates.""" 

3147 return self._com.Direction 

3148 

3149 @property 

3150 def meas_point_index(self) -> int: 

3151 """One-based measurement-point index in the MeasPoints collection.""" 

3152 return int(self._com.MeasPointIndex) 

3153 

3154 @property 

3155 def mode(self) -> MeasurementLocationMode: 

3156 """Measurement-location mode.""" 

3157 return MeasurementLocationMode(self._com.Mode) 

3158 

3159 @property 

3160 def normal(self) -> Any: 

3161 """Normal vector or vectors in object coordinates.""" 

3162 return self._com.Normal 

3163 

3164 @property 

3165 def status(self) -> ScanStatus: 

3166 """Measurement status of the referenced point.""" 

3167 return ScanStatus(int(self._com.Status)) 

3168 

3169 

3170class Profile: 

3171 """Wrapper around a COM profile object.""" 

3172 

3173 def __init__(self, obj: Any) -> None: 

3174 self._com = obj 

3175 

3176 @property 

3177 def color(self) -> Any: 

3178 """Profile color code.""" 

3179 return self._com.Color 

3180 

3181 @property 

3182 def lines(self) -> Any: 

3183 """Profile line coordinates in video units.""" 

3184 return self._com.Lines 

3185 

3186 @property 

3187 def type(self) -> ProfileType: 

3188 """Profile type.""" 

3189 return ProfileType(self._com.Type) 

3190 

3191 

3192class DistanceSensorControl: 

3193 """Wrapper around COM distance-sensor control.""" 

3194 

3195 def __init__(self, obj: Any) -> None: 

3196 self._com = obj 

3197 

3198 @property 

3199 def connected(self) -> bool: 

3200 """Whether the geometry scan unit is connected.""" 

3201 return bool(self._com.Connected) 

3202 

3203 @property 

3204 def filter_on(self) -> bool: 

3205 """Whether the geometry scan-unit filter is enabled.""" 

3206 return bool(self._com.FilterOn) 

3207 

3208 @property 

3209 def laser_on(self) -> bool: 

3210 """Whether the geometry laser is on.""" 

3211 return bool(self._com.LaserOn) 

3212 

3213 @property 

3214 def scan_head_device(self) -> "ScanHeadDevice": 

3215 """Associated scan-head device.""" 

3216 return ScanHeadDevice(self._com.ScanHeadDevice) 

3217 

3218 @property 

3219 def type(self) -> DistanceSensorType: 

3220 """Distance-sensor type.""" 

3221 return DistanceSensorType(self._com.Type) 

3222 

3223 

3224class ScanHeadLaserFocus: 

3225 """Wrapper around COM scan-head focus settings.""" 

3226 

3227 def __init__(self, obj: Any) -> None: 

3228 self._com = obj 

3229 

3230 def get_depth_of_focus_range(self, focus_position: float, depth_of_sharp_factor: float) -> Any: 

3231 """Return the depth-of-focus range.""" 

3232 return self._com.GetDepthOfFocusRange(focus_position, depth_of_sharp_factor, 0, 0) 

3233 

3234 @property 

3235 def depth_of_focus_factor(self) -> float: 

3236 """Depth-of-focus factor.""" 

3237 return float(self._com.DepthOfFocusFactor) 

3238 

3239 @property 

3240 def focal_length(self) -> float: 

3241 """Objective focal length.""" 

3242 return float(self._com.FocalLength) 

3243 

3244 @property 

3245 def focus_command_time(self) -> float: 

3246 """Time per focus command in seconds.""" 

3247 return float(self._com.FocusCommandTime) 

3248 

3249 @property 

3250 def focus_sharpness(self) -> float: 

3251 """Focus sharpness.""" 

3252 return float(self._com.FocusSharpness) 

3253 

3254 @property 

3255 def focus_step_width(self) -> float: 

3256 """Focus step width in meters.""" 

3257 return float(self._com.FocusStepWidth) 

3258 

3259 @property 

3260 def lens_to_mirror_distance(self) -> float: 

3261 """Distance from objective to first mirror in meters.""" 

3262 return float(self._com.LensToMirrorDistance) 

3263 

3264 @property 

3265 def min_focus_distance(self) -> float: 

3266 """Minimum focus distance in meters.""" 

3267 return float(self._com.MinFocusDistance) 

3268 

3269 @property 

3270 def scan_head_device(self) -> "ScanHeadDevice": 

3271 """Associated scan-head device.""" 

3272 return ScanHeadDevice(self._com.ScanHeadDevice) 

3273 

3274 @property 

3275 def time_per_focus_step(self) -> float: 

3276 """Time per focus step in seconds.""" 

3277 return float(self._com.TimePerFocusStep) 

3278 

3279 

3280class ScanHeadScanner: 

3281 """Wrapper around COM scan-head scanner settings.""" 

3282 

3283 def __init__(self, obj: Any) -> None: 

3284 self._com = obj 

3285 

3286 def get_laser_origin(self) -> Any: 

3287 """Return the laser origin at 0°/0° beam direction.""" 

3288 return self._com.GetLaserOrigin() 

3289 

3290 def get_mirror_times_x(self, step: float) -> Any: 

3291 """Return moving and waiting times for a horizontal step.""" 

3292 return self._com.GetMirrorTimesX(step) 

3293 

3294 def get_mirror_times_y(self, step: float) -> Any: 

3295 """Return moving and waiting times for a vertical step.""" 

3296 return self._com.GetMirrorTimesY(step) 

3297 

3298 @property 

3299 def cos_correction_active_x(self) -> bool: 

3300 """Whether angle correction is active in the horizontal direction.""" 

3301 return bool(self._com.CosCorrectionActiveX) 

3302 

3303 @property 

3304 def cos_correction_active_y(self) -> bool: 

3305 """Whether angle correction is active in the vertical direction.""" 

3306 return bool(self._com.CosCorrectionActiveY) 

3307 

3308 @property 

3309 def cos_correction_possible_x(self) -> bool: 

3310 """Whether angle correction is possible in the horizontal direction.""" 

3311 return bool(self._com.CosCorrectionPossibleX) 

3312 

3313 @property 

3314 def cos_correction_possible_y(self) -> bool: 

3315 """Whether angle correction is possible in the vertical direction.""" 

3316 return bool(self._com.CosCorrectionPossibleY) 

3317 

3318 @property 

3319 def inverted_x(self) -> bool: 

3320 """Whether horizontal scanner polarity is inverted.""" 

3321 return bool(self._com.InvertedX) 

3322 

3323 @property 

3324 def inverted_y(self) -> bool: 

3325 """Whether vertical scanner polarity is inverted.""" 

3326 return bool(self._com.InvertedY) 

3327 

3328 @property 

3329 def mirror_distance(self) -> float: 

3330 """Distance between the two scanner mirrors in meters.""" 

3331 return float(self._com.MirrorDistance) 

3332 

3333 @property 

3334 def preferred_scan_direction(self) -> PreferredScanDirection: 

3335 """Preferred scan direction.""" 

3336 return PreferredScanDirection(self._com.PreferredScanDirection) 

3337 

3338 @property 

3339 def quantity(self) -> PhysicalQuantity: 

3340 """Physical quantity of the scanning head.""" 

3341 return PhysicalQuantity(self._com.Quantity) 

3342 

3343 @property 

3344 def quantity_max_x(self) -> float: 

3345 """Maximum horizontal scanner value.""" 

3346 return float(self._com.QuantityMaxX) 

3347 

3348 @property 

3349 def quantity_max_y(self) -> float: 

3350 """Maximum vertical scanner value.""" 

3351 return float(self._com.QuantityMaxY) 

3352 

3353 @property 

3354 def scan_head_device(self) -> "ScanHeadDevice": 

3355 """Associated scan-head device.""" 

3356 return ScanHeadDevice(self._com.ScanHeadDevice) 

3357 

3358 @property 

3359 def swapped_xy(self) -> bool: 

3360 """Whether horizontal and vertical scanners are swapped.""" 

3361 return bool(self._com.SwappedXY) 

3362 

3363 

3364class ScanHeadVideo: 

3365 """Wrapper around COM scan-head video settings.""" 

3366 

3367 def __init__(self, obj: Any) -> None: 

3368 self._com = obj 

3369 

3370 @property 

3371 def flipped_x(self) -> bool: 

3372 """Whether the video image is flipped horizontally.""" 

3373 return bool(self._com.FlippedX) 

3374 

3375 @property 

3376 def flipped_y(self) -> bool: 

3377 """Whether the video image is flipped vertically.""" 

3378 return bool(self._com.FlippedY) 

3379 

3380 @property 

3381 def scan_head_device(self) -> "ScanHeadDevice": 

3382 """Associated scan-head device.""" 

3383 return ScanHeadDevice(self._com.ScanHeadDevice) 

3384 

3385 @property 

3386 def type_value(self) -> int: 

3387 """Video type as a raw bitmask.""" 

3388 return int(self._com.Type) 

3389 

3390 @property 

3391 def type(self) -> ScanHeadVideoType: 

3392 """Video type/capability flags.""" 

3393 return ScanHeadVideoType(self.type_value) 

3394 

3395 

3396class ScanHeadDevice: 

3397 """Wrapper around a COM scan-head device.""" 

3398 

3399 def __init__(self, obj: Any) -> None: 

3400 self._com = obj 

3401 

3402 @property 

3403 def caps_value(self) -> int: 

3404 """Scanning-head capabilities as a raw bitmask.""" 

3405 return int(self._com.Caps) 

3406 

3407 @property 

3408 def caps(self) -> ScanHeadCaps: 

3409 """Scanning-head capabilities as an ``IntFlag``.""" 

3410 return ScanHeadCaps(self.caps_value) 

3411 

3412 @property 

3413 def distance_sensor_control(self) -> DistanceSensorControl: 

3414 """Distance-sensor control object.""" 

3415 return DistanceSensorControl(self._com.DistanceSensorControl) 

3416 

3417 @property 

3418 def focus(self) -> ScanHeadLaserFocus: 

3419 """Scanning-head focus object.""" 

3420 return ScanHeadLaserFocus(self._com.Focus) 

3421 

3422 @property 

3423 def lens_magnification(self) -> float: 

3424 """Objective magnification.""" 

3425 return float(self._com.LensMagnification) 

3426 

3427 @property 

3428 def pan_tilt_head(self) -> bool: 

3429 """Whether a pan-tilt head is used.""" 

3430 return bool(self._com.PanTiltHead) 

3431 

3432 @property 

3433 def roll_angle(self) -> float: 

3434 """Roll angle of the scanning head.""" 

3435 return float(self._com.RollAngle) 

3436 

3437 @property 

3438 def scanner(self) -> ScanHeadScanner: 

3439 """Scanning-head scanner object.""" 

3440 return ScanHeadScanner(self._com.Scanner) 

3441 

3442 @property 

3443 def sensor_head_name(self) -> str: 

3444 """Name of the active sensor head.""" 

3445 return self._com.SensorHeadName 

3446 

3447 @property 

3448 def sensor_head_names(self) -> list[str]: 

3449 """All possible sensor-head names.""" 

3450 return list(self._com.SensorHeadNames) 

3451 

3452 @property 

3453 def video(self) -> ScanHeadVideo: 

3454 """Scanning-head video object.""" 

3455 return ScanHeadVideo(self._com.Video) 

3456 

3457 @property 

3458 def scan_head_devices(self) -> list["ScanHeadDevice"]: 

3459 """Parent scan-head-device collection as wrapped objects.""" 

3460 return [ScanHeadDevice(v) for v in self._com.ScanHeadDevices] 

3461 

3462 

3463class ScanHeadDevicesInfo: 

3464 """Wrapper around COM scan-head-devices info.""" 

3465 

3466 def __init__(self, obj: Any) -> None: 

3467 self._com = obj 

3468 

3469 @property 

3470 def name(self) -> str: 

3471 """Info-object name.""" 

3472 return self._com.Name 

3473 

3474 @property 

3475 def scan_head_devices(self) -> list[ScanHeadDevice]: 

3476 """Scan-head devices.""" 

3477 return [ScanHeadDevice(v) for v in self._com.ScanHeadDevices] 

3478 

3479 @property 

3480 def scan_head_name(self) -> str: 

3481 """Scanning-head name.""" 

3482 return self._com.ScanHeadName 

3483 

3484 @property 

3485 def scan_head_type(self) -> ScanHeadType: 

3486 """Scanning-head type.""" 

3487 return ScanHeadType(self._com.ScanHeadType) 

3488 

3489 @property 

3490 def type(self) -> Any: 

3491 """Info-object type identifier.""" 

3492 return self._com.Type 

3493 

3494 

3495class SpectrogramInfo: 

3496 """Wrapper around COM spectrogram/campbell-diagram info.""" 

3497 

3498 def __init__(self, obj: Any) -> None: 

3499 self._com = obj 

3500 

3501 @property 

3502 def bandwidth(self) -> float: 

3503 """Bandwidth used for spectrogram FFT calculation.""" 

3504 return float(self._com.Bandwidth) 

3505 

3506 @property 

3507 def bandwidth_from(self) -> float: 

3508 """Lower bandwidth limit.""" 

3509 return float(self._com.BandwidthFrom) 

3510 

3511 @property 

3512 def bandwidth_to(self) -> float: 

3513 """Upper bandwidth limit.""" 

3514 return float(self._com.BandwidthTo) 

3515 

3516 @property 

3517 def effective_sample_count(self) -> int: 

3518 """Effective sample count used for FFT calculation.""" 

3519 return int(self._com.EffectiveSampleCount) 

3520 

3521 @property 

3522 def fft_count(self) -> int: 

3523 """Number of FFTs in the resulting spectrogram.""" 

3524 return int(self._com.FFTCount) 

3525 

3526 @property 

3527 def fft_lines(self) -> int: 

3528 """Number of FFT lines.""" 

3529 return int(self._com.FFTLines) 

3530 

3531 @property 

3532 def frequency_resolution(self) -> float: 

3533 """Frequency resolution in hertz.""" 

3534 return float(self._com.FrequencyResolution) 

3535 

3536 @property 

3537 def name(self) -> str: 

3538 """Info-object name.""" 

3539 return self._com.Name 

3540 

3541 @property 

3542 def overlap(self) -> float: 

3543 """FFT overlap in percent.""" 

3544 return float(self._com.Overlap) 

3545 

3546 @property 

3547 def rpm_channel(self) -> str: 

3548 """Name of the RPM channel.""" 

3549 return self._com.RPMChannel 

3550 

3551 @property 

3552 def sample_count(self) -> int: 

3553 """Number of samples used to calculate an FFT.""" 

3554 return int(self._com.SampleCount) 

3555 

3556 @property 

3557 def sample_rate(self) -> float: 

3558 """Time-trace sample rate in hertz.""" 

3559 return float(self._com.SampleRate) 

3560 

3561 @property 

3562 def sample_rate_bandwidth_ratio(self) -> float: 

3563 """Ratio between sample rate and bandwidth.""" 

3564 return float(self._com.SampleRateBandwidthRatio) 

3565 

3566 @property 

3567 def time_from(self) -> float: 

3568 """Start of the time range considered.""" 

3569 return float(self._com.TimeFrom) 

3570 

3571 @property 

3572 def time_to(self) -> float: 

3573 """End of the time range considered.""" 

3574 return float(self._com.TimeTo) 

3575 

3576 @property 

3577 def total_sample_count(self) -> int: 

3578 """Total number of samples available.""" 

3579 return int(self._com.TotalSampleCount) 

3580 

3581 @property 

3582 def type(self) -> Any: 

3583 """Spectrogram type identifier.""" 

3584 return self._com.Type 

3585 

3586 @property 

3587 def used_fft_lines(self) -> int: 

3588 """Number of used FFT lines.""" 

3589 return int(self._com.UsedFFTLines) 

3590 

3591 @property 

3592 def window_function(self) -> WindowFunction: 

3593 """Window function used before FFT calculation.""" 

3594 return WindowFunction(self._com.WindowFunction) 

3595 

3596 @property 

3597 def window_parameter(self) -> Any: 

3598 """Window-function parameter(s).""" 

3599 return self._com.WindowParameter 

3600 

3601 

3602class Texture: 

3603 """Wrapper around a COM texture object.""" 

3604 

3605 def __init__(self, obj: Any) -> None: 

3606 self._com = obj 

3607 

3608 @property 

3609 def height(self) -> int: 

3610 """Texture height in pixels.""" 

3611 return int(self._com.Height) 

3612 

3613 @property 

3614 def image(self) -> bytes: 

3615 """Texture image bytes in BMP file format.""" 

3616 return bytes(self._com.Image) 

3617 

3618 @property 

3619 def width(self) -> int: 

3620 """Texture width in pixels.""" 

3621 return int(self._com.Width) 

3622 

3623 

3624class VideoBitmap: 

3625 """Wrapper around COM video-bitmap info.""" 

3626 

3627 def __init__(self, obj: Any) -> None: 

3628 self._com = obj 

3629 

3630 def get_3d_video_offset(self) -> Any: 

3631 """Get the video-position offset in 3D coordinates.""" 

3632 return self._com.Get3DVideoOffset() 

3633 

3634 def get_image_rect(self) -> Any: 

3635 """Get the image rectangle in video coordinates.""" 

3636 return self._com.GetImageRect() 

3637 

3638 def get_video_offset(self) -> Any: 

3639 """Get the video-position offset in video coordinates.""" 

3640 return self._com.GetVideoOffset() 

3641 

3642 def load_image_file(self, file_name: str) -> None: 

3643 """Load an image from the given file.""" 

3644 self._com.LoadImageFile(file_name) 

3645 

3646 def save(self) -> None: 

3647 """Commit changes to the underlying file.""" 

3648 self._com.Save() 

3649 

3650 def set_image(self, image_bytes: bytes) -> None: 

3651 """Set the image from an array of bytes.""" 

3652 self._com.SetImage(image_bytes) 

3653 

3654 def image_bytes( 

3655 self, fmt: GraphicFormatType = GraphicFormatType.BITMAP 

3656 ) -> tuple[bytes, int, int]: 

3657 """Return image bytes and dimensions. 

3658 

3659 Parameters 

3660 ---------- 

3661 fmt 

3662 Graphic format to request from the COM interface. 

3663 

3664 Returns 

3665 ------- 

3666 tuple[bytes, int, int] 

3667 Image bytes, width, and height. 

3668 """ 

3669 data, width, height = self._com.Image(fmt.value) 

3670 return bytes(data), int(width), int(height) 

3671 

3672 def get_image(self) -> Image.Image: 

3673 """Return a ``PIL.Image.Image`` object for the bitmap.""" 

3674 return Image.open(BytesIO(self.image_bytes()[0])) 

3675 

3676 @property 

3677 def name(self) -> str: 

3678 """Info-object name.""" 

3679 return self._com.Name 

3680 

3681 @property 

3682 def texture_columns(self) -> int: 

3683 """Number of columns in a stitched combined-file image.""" 

3684 return int(self._com.TextureColumns) 

3685 

3686 @property 

3687 def texture_coordinates(self) -> Any: 

3688 """Texture coordinates for scan points.""" 

3689 return self._com.TextureCoordinates 

3690 

3691 @property 

3692 def texture_count(self) -> int: 

3693 """Number of stitched video bitmaps.""" 

3694 return int(self._com.TextureCount) 

3695 

3696 @property 

3697 def texture_rows(self) -> int: 

3698 """Number of rows in a stitched combined-file image.""" 

3699 return int(self._com.TextureRows) 

3700 

3701 @property 

3702 def type(self) -> Any: 

3703 """Info-object type identifier.""" 

3704 return self._com.Type 

3705 

3706 

3707class LensCalibration: 

3708 """Wrapper around COM lens calibration information.""" 

3709 

3710 def __init__(self, obj: Any) -> None: 

3711 self._com = obj 

3712 

3713 @property 

3714 def caps_value(self) -> int: 

3715 """Lens-calibration capabilities as a raw bitmask.""" 

3716 return int(self._com.Caps) 

3717 

3718 @property 

3719 def caps(self) -> LensCalibrationCaps: 

3720 """Lens-calibration capabilities as an ``IntFlag``.""" 

3721 return LensCalibrationCaps(self.caps_value) 

3722 

3723 @property 

3724 def distance_x(self) -> float: 

3725 """Horizontal calibration distance in meters.""" 

3726 return float(self._com.DistanceX) 

3727 

3728 @property 

3729 def distance_y(self) -> float: 

3730 """Vertical calibration distance in meters.""" 

3731 return float(self._com.DistanceY) 

3732 

3733 @property 

3734 def magnification_factor_x(self) -> float: 

3735 """Horizontal magnification factor.""" 

3736 return float(self._com.MagnificationFactorX) 

3737 

3738 @property 

3739 def magnification_factor_y(self) -> float: 

3740 """Vertical magnification factor.""" 

3741 return float(self._com.MagnificationFactorY) 

3742 

3743 @property 

3744 def name(self) -> str: 

3745 """Calibration name.""" 

3746 return self._com.Name 

3747 

3748 @property 

3749 def pixels_x(self) -> int: 

3750 """Horizontal calibration distance in pixels.""" 

3751 return int(self._com.PixelsX) 

3752 

3753 @property 

3754 def pixels_y(self) -> int: 

3755 """Vertical calibration distance in pixels.""" 

3756 return int(self._com.PixelsY) 

3757 

3758 

3759class CameraSize: 

3760 """Wrapper around COM camera-size information.""" 

3761 

3762 def __init__(self, obj: Any) -> None: 

3763 self._com = obj 

3764 

3765 @property 

3766 def chip_x(self) -> float: 

3767 """Horizontal camera-chip size in meters.""" 

3768 return float(self._com.ChipX) 

3769 

3770 @property 

3771 def chip_y(self) -> float: 

3772 """Vertical camera-chip size in meters.""" 

3773 return float(self._com.ChipY) 

3774 

3775 @property 

3776 def image_x(self) -> int: 

3777 """Horizontal pixel count of the camera chip.""" 

3778 return int(self._com.ImageX) 

3779 

3780 @property 

3781 def image_y(self) -> int: 

3782 """Vertical pixel count of the camera chip.""" 

3783 return int(self._com.ImageY) 

3784 

3785 

3786class VideoMappingInfo: 

3787 """Wrapper around COM video-mapping information.""" 

3788 

3789 def __init__(self, obj: Any) -> None: 

3790 self._com = obj 

3791 

3792 def coord_xy_from_video(self, video_x: float, video_y: float) -> tuple[float, float]: 

3793 """Retrieve world coordinates from video coordinates.""" 

3794 coord_x, coord_y = self._com.CoordXYFromVideo(video_x, video_y) 

3795 return float(coord_x), float(coord_y) 

3796 

3797 def video_xy_from_coord(self, coord_x: float, coord_y: float) -> tuple[float, float]: 

3798 """Retrieve video coordinates from world coordinates.""" 

3799 video_x, video_y = self._com.VideoXYFromCoord(coord_x, coord_y) 

3800 return float(video_x), float(video_y) 

3801 

3802 def save(self) -> None: 

3803 """Save modifications to the underlying file.""" 

3804 self._com.Save() 

3805 

3806 @property 

3807 def active_calibration(self) -> str: 

3808 """Name of the active objective calibration.""" 

3809 return self._com.ActiveCalibration 

3810 

3811 @property 

3812 def calibrations(self) -> list[LensCalibration]: 

3813 """Available lens calibrations.""" 

3814 return [LensCalibration(c) for c in self._com.Calibrations] 

3815 

3816 @property 

3817 def camera_size(self) -> CameraSize: 

3818 """Camera-size object.""" 

3819 return CameraSize(self._com.CameraSize) 

3820 

3821 @property 

3822 def name(self) -> str: 

3823 """Info-object name.""" 

3824 return self._com.Name 

3825 

3826 @property 

3827 def type(self) -> Any: 

3828 """Info-object type identifier.""" 

3829 return self._com.Type 

3830 

3831 

3832class XAxis: 

3833 """Wrapper around a COM x-axis descriptor.""" 

3834 

3835 def __init__(self, obj: Any) -> None: 

3836 self._com = obj 

3837 

3838 @property 

3839 def max(self) -> float: 

3840 """Maximum x-axis value.""" 

3841 return float(self._com.Max) 

3842 

3843 @property 

3844 def max_count(self) -> int: 

3845 """Number of x-axis samples.""" 

3846 return int(self._com.MaxCount) 

3847 

3848 @property 

3849 def min(self) -> float: 

3850 """Minimum x-axis value.""" 

3851 return float(self._com.Min) 

3852 

3853 @property 

3854 def name(self) -> str: 

3855 """X-axis label.""" 

3856 return self._com.Name 

3857 

3858 @property 

3859 def unit(self) -> str: 

3860 """X-axis unit.""" 

3861 return self._com.Unit 

3862 

3863 

3864class YAxis: 

3865 """Wrapper around a COM y-axis descriptor.""" 

3866 

3867 def __init__(self, obj: Any) -> None: 

3868 self._com = obj 

3869 

3870 @property 

3871 def max(self) -> float: 

3872 """Maximum y-axis value.""" 

3873 return float(self._com.Max) 

3874 

3875 @property 

3876 def min(self) -> float: 

3877 """Minimum y-axis value.""" 

3878 return float(self._com.Min) 

3879 

3880 @property 

3881 def name(self) -> str: 

3882 """Y-axis label.""" 

3883 return self._com.Name 

3884 

3885 @property 

3886 def unit(self) -> str: 

3887 """Y-axis unit.""" 

3888 return self._com.Unit 

3889 

3890 

3891class Domain: 

3892 """Wrapper around a COM domain.""" 

3893 

3894 def __init__(self, obj: Any) -> None: 

3895 self._com = obj 

3896 

3897 @property 

3898 def channels(self) -> list["Channel"]: 

3899 """Channels in the domain.""" 

3900 return [Channel(c) for c in self._com.Channels] 

3901 

3902 @property 

3903 def name(self) -> str: 

3904 """Domain name.""" 

3905 return self._com.Name 

3906 

3907 @property 

3908 def type(self) -> DomainType: 

3909 """Domain type.""" 

3910 return DomainType(self._com.Type) 

3911 

3912 

3913class Channel: 

3914 """Wrapper around a COM signal channel.""" 

3915 

3916 def __init__(self, obj: Any) -> None: 

3917 self._com = obj 

3918 

3919 @property 

3920 def caps_value(self) -> int: 

3921 """Channel capabilities as a raw bitmask.""" 

3922 return int(self._com.Caps) 

3923 

3924 @property 

3925 def caps(self) -> ChannelCaps: 

3926 """Channel capabilities as an ``IntFlag``.""" 

3927 return ChannelCaps(self.caps_value) 

3928 

3929 @property 

3930 def caps_set(self) -> list[ChannelCaps]: 

3931 """Individual channel capability flags that are set.""" 

3932 return _flags_list(self.caps_value, ChannelCaps) 

3933 

3934 @property 

3935 def domain(self) -> Domain: 

3936 """Parent domain.""" 

3937 return Domain(self._com.Domain) 

3938 

3939 @property 

3940 def name(self) -> str: 

3941 """Channel name.""" 

3942 return self._com.Name 

3943 

3944 @property 

3945 def signals(self) -> list["Signal"]: 

3946 """Signals available on the channel.""" 

3947 return [Signal(s) for s in self._com.Signals] 

3948 

3949 

3950class Signal: 

3951 """Wrapper around a COM signal.""" 

3952 

3953 def __init__(self, obj: Any) -> None: 

3954 self._com = obj 

3955 

3956 @property 

3957 def channel(self) -> Channel: 

3958 """Parent channel.""" 

3959 return Channel(self._com.Channel) 

3960 

3961 @property 

3962 def description(self) -> "SignalDescription": 

3963 """Signal description.""" 

3964 return SignalDescription(self._com.Description) 

3965 

3966 @property 

3967 def displays(self) -> list["Display"]: 

3968 """Available displays for the signal.""" 

3969 return [Display(d) for d in self._com.Displays] 

3970 

3971 @property 

3972 def name(self) -> str: 

3973 """Signal name.""" 

3974 return self._com.Name 

3975 

3976 

3977class Display: 

3978 """Wrapper around a COM signal display.""" 

3979 

3980 def __init__(self, obj: Any) -> None: 

3981 self._com = obj 

3982 

3983 @property 

3984 def name(self) -> str: 

3985 """Display name.""" 

3986 return self._com.Name 

3987 

3988 @property 

3989 def signal(self) -> Signal: 

3990 """Parent signal.""" 

3991 return Signal(self._com.Signal) 

3992 

3993 @property 

3994 def type(self) -> DisplayType: 

3995 """Display type.""" 

3996 return DisplayType(self._com.Type) 

3997 

3998 

3999class Attribute: 

4000 """Wrapper around a COM signal attribute.""" 

4001 

4002 def __init__(self, obj: Any) -> None: 

4003 self._com = obj 

4004 

4005 @property 

4006 def description(self) -> str: 

4007 """Attribute description.""" 

4008 return self._com.Description 

4009 

4010 @property 

4011 def name(self) -> str: 

4012 """Attribute name.""" 

4013 return self._com.Name 

4014 

4015 @property 

4016 def quantity(self) -> Any: 

4017 """Physical quantity of the attribute value.""" 

4018 return self._com.Quantity 

4019 

4020 @property 

4021 def unit(self) -> str: 

4022 """Unit of the attribute value.""" 

4023 return self._com.Unit 

4024 

4025 @property 

4026 def value(self) -> Any: 

4027 """Attribute value.""" 

4028 return self._com.Value 

4029 

4030 

4031class DegreeOfFreedomID: 

4032 """Wrapper around a COM degree-of-freedom identifier.""" 

4033 

4034 def __init__(self, obj: Any) -> None: 

4035 self._com = obj 

4036 

4037 @property 

4038 def channel_name(self) -> str: 

4039 """Channel name for the DOF.""" 

4040 return self._com.ChannelName 

4041 

4042 @property 

4043 def direction(self) -> DOFDirection: 

4044 """DOF direction.""" 

4045 return DOFDirection(self._com.Direction) 

4046 

4047 @property 

4048 def node(self) -> Any: 

4049 """DOF node number or identifier.""" 

4050 return self._com.Node 

4051 

4052 @property 

4053 def node_description(self) -> str: 

4054 """Node description.""" 

4055 return self._com.NodeDescription 

4056 

4057 @property 

4058 def quantity(self) -> Any: 

4059 """Physical quantity of the DOF channel.""" 

4060 return self._com.Quantity 

4061 

4062 @property 

4063 def unit(self) -> str: 

4064 """Unit of the DOF channel.""" 

4065 return self._com.Unit 

4066 

4067 

4068class SignalXAxis: 

4069 """Wrapper around a COM signal x-axis descriptor.""" 

4070 

4071 def __init__(self, obj: Any) -> None: 

4072 self._com = obj 

4073 

4074 @property 

4075 def max(self) -> float: 

4076 """Maximum x-axis value.""" 

4077 return float(self._com.Max) 

4078 

4079 @property 

4080 def max_count(self) -> int: 

4081 """Number of x-axis samples.""" 

4082 return int(self._com.MaxCount) 

4083 

4084 @property 

4085 def min(self) -> float: 

4086 """Minimum x-axis value.""" 

4087 return float(self._com.Min) 

4088 

4089 @property 

4090 def name(self) -> str: 

4091 """X-axis label.""" 

4092 return self._com.Name 

4093 

4094 @property 

4095 def unit(self) -> str: 

4096 """X-axis unit.""" 

4097 return self._com.Unit 

4098 

4099 

4100class SignalYAxis: 

4101 """Wrapper around a COM signal y-axis descriptor.""" 

4102 

4103 def __init__(self, obj: Any) -> None: 

4104 self._com = obj 

4105 

4106 @property 

4107 def max(self) -> float: 

4108 """Maximum y-axis value.""" 

4109 return float(self._com.Max) 

4110 

4111 @property 

4112 def min(self) -> float: 

4113 """Minimum y-axis value.""" 

4114 return float(self._com.Min) 

4115 

4116 @property 

4117 def name(self) -> str: 

4118 """Y-axis label.""" 

4119 return self._com.Name 

4120 

4121 @property 

4122 def unit(self) -> str: 

4123 """Y-axis unit.""" 

4124 return self._com.Unit 

4125 

4126 

4127class SignalDescription: 

4128 """Wrapper around COM signal metadata.""" 

4129 

4130 def __init__(self, obj: Any) -> None: 

4131 self._com = obj 

4132 

4133 @property 

4134 def attributes(self) -> list[Attribute]: 

4135 """Additional attributes attached to the signal description.""" 

4136 return [Attribute(a) for a in self._com.Attributes] 

4137 

4138 @property 

4139 def complex(self) -> bool: 

4140 """Whether the signal is complex-valued.""" 

4141 return bool(self._com.Complex) 

4142 

4143 @property 

4144 def data_type(self) -> DataType: 

4145 """Signal data type.""" 

4146 return DataType(self._com.DataType) 

4147 

4148 @property 

4149 def db_reference(self) -> float: 

4150 """Zero-dB reference value.""" 

4151 return float(self._com.DbReference) 

4152 

4153 @property 

4154 def domain_type(self) -> DomainType: 

4155 """Domain type of the signal.""" 

4156 return DomainType(self._com.DomainType) 

4157 

4158 @property 

4159 def function_type(self) -> FunctionType: 

4160 """Function type of the signal.""" 

4161 return FunctionType(self._com.FunctionType) 

4162 

4163 @property 

4164 def name(self) -> str: 

4165 """Signal description name.""" 

4166 return self._com.Name 

4167 

4168 @property 

4169 def power_signal(self) -> bool: 

4170 """Whether the signal is a power signal.""" 

4171 return bool(self._com.PowerSignal) 

4172 

4173 @property 

4174 def reference_dofs(self) -> list[DegreeOfFreedomID]: 

4175 """Reference degree-of-freedom identifiers.""" 

4176 return [DegreeOfFreedomID(d) for d in self._com.ReferenceDOFs] 

4177 

4178 @property 

4179 def response_dofs(self) -> list[DegreeOfFreedomID]: 

4180 """Response degree-of-freedom identifiers.""" 

4181 return [DegreeOfFreedomID(d) for d in self._com.ResponseDOFs] 

4182 

4183 @property 

4184 def rms_correction_factor(self) -> float: 

4185 """RMS correction factor.""" 

4186 return float(self._com.RMSCorrectionFactor) 

4187 

4188 @property 

4189 def x_axis(self) -> SignalXAxis: 

4190 """X-axis descriptor.""" 

4191 return SignalXAxis(self._com.XAxis) 

4192 

4193 @property 

4194 def y_axis(self) -> SignalYAxis: 

4195 """Y-axis descriptor.""" 

4196 return SignalYAxis(self._com.YAxis) 

4197 

4198 

4199class DataPoint: 

4200 """Wrapper around a COM data point.""" 

4201 

4202 def __init__(self, obj: Any) -> None: 

4203 self._com = obj 

4204 

4205 @property 

4206 def average_count(self) -> int: 

4207 """Number of traces that contributed to this averaged result.""" 

4208 return int(self._com.AverageCount) 

4209 

4210 @property 

4211 def meas_point(self) -> MeasPoint: 

4212 """Measurement point corresponding to this data point.""" 

4213 return MeasPoint(self._com.MeasPoint) 

4214 

4215 @property 

4216 def point_domain(self) -> "PointDomain": 

4217 """Parent point domain.""" 

4218 return PointDomain(self._com.PointDomain) 

4219 

4220 def get_3d_data(self, display: Any, frame: int = 0) -> np.ndarray: 

4221 """Retrieve 3D data for a given display and frame.""" 

4222 disp_com = _unwrap_com(display) 

4223 return np.asarray(self._com.Get3DData(disp_com, int(frame))) 

4224 

4225 def get_3d_data_section( 

4226 self, 

4227 display: Any, 

4228 frame: int, 

4229 average: int, 

4230 first_index: int, 

4231 last_index: int, 

4232 ) -> np.ndarray: 

4233 """Retrieve a section of 3D data for a given display.""" 

4234 disp_com = _unwrap_com(display) 

4235 return np.asarray( 

4236 self._com.Get3DDataSection( 

4237 disp_com, 

4238 int(frame), 

4239 int(average), 

4240 int(first_index), 

4241 int(last_index), 

4242 ) 

4243 ) 

4244 

4245 def get_data(self, display: Any, frame: int = 0) -> np.ndarray: 

4246 """Retrieve data for a given display and frame.""" 

4247 disp_com = _unwrap_com(display) 

4248 return np.asarray(self._com.GetData(disp_com, int(frame))) 

4249 

4250 def get_data_section( 

4251 self, 

4252 display: Any, 

4253 frame: int, 

4254 average: int, 

4255 first_index: int, 

4256 last_index: int, 

4257 ) -> np.ndarray: 

4258 """Retrieve a section of data for a given display.""" 

4259 disp_com = _unwrap_com(display) 

4260 return np.asarray( 

4261 self._com.GetDataSection( 

4262 disp_com, 

4263 int(frame), 

4264 int(average), 

4265 int(first_index), 

4266 int(last_index), 

4267 ) 

4268 ) 

4269 

4270 def get_frame_boundaries(self, display: Any, average: int = 1) -> np.ndarray: 

4271 """Retrieve frame boundaries for a given time signal.""" 

4272 disp_com = _unwrap_com(display) 

4273 return np.asarray(self._com.GetFrameBoundaries(disp_com, int(average))) 

4274 

4275 def get_frames(self, display: Any) -> int: 

4276 """Retrieve the frame count for a given display.""" 

4277 disp_com = _unwrap_com(display) 

4278 return int(self._com.GetFrames(disp_com)) 

4279 

4280 def get_scan_status(self, display: Any) -> ScanStatus: 

4281 """Retrieve the scan status for a given display.""" 

4282 disp_com = _unwrap_com(display) 

4283 return ScanStatus(int(self._com.GetScanStatus(disp_com))) 

4284 

4285 

4286class PointDomain: 

4287 """Wrapper around a COM point domain.""" 

4288 

4289 def __init__(self, obj: Any) -> None: 

4290 self._com = obj 

4291 

4292 def get_x_axis(self) -> XAxis: 

4293 """Return an x-axis descriptor for the point domain.""" 

4294 return XAxis(self._com.GetXAxis()) 

4295 

4296 def get_y_axes(self) -> list[YAxis]: 

4297 """Return y-axis descriptors for the point domain.""" 

4298 return [YAxis(y) for y in self._com.GetYAxes()] 

4299 

4300 @property 

4301 def channels(self) -> list[Channel]: 

4302 """Channels in the point domain.""" 

4303 return [Channel(c) for c in self._com.Channels] 

4304 

4305 @property 

4306 def data_points(self) -> list[DataPoint]: 

4307 """Data points in the point domain.""" 

4308 return [DataPoint(dp) for dp in self._com.DataPoints] 

4309 

4310 @property 

4311 def name(self) -> str: 

4312 """Point-domain name.""" 

4313 return self._com.Name 

4314 

4315 @property 

4316 def rms_correction_factor(self) -> float: 

4317 """RMS correction factor for the point domain.""" 

4318 return float(self._com.RmsCorrectionFactor) 

4319 

4320 @property 

4321 def type(self) -> DomainType: 

4322 """Point-domain type.""" 

4323 return DomainType(self._com.Type) 

4324 

4325 

4326class PolyFile: 

4327 """High-level wrapper around a Polytec PSV file COM object.""" 

4328 

4329 def __init__(self, obj_or_path: str | PathLike[str] | Any) -> None: 

4330 """Open a Polytec file or wrap an existing COM file object. 

4331 

4332 Parameters 

4333 ---------- 

4334 obj_or_path 

4335 Either a filesystem path to a Polytec file, or an existing COM file 

4336 object returned by the PolyFile COM interface. 

4337 

4338 Raises 

4339 ------ 

4340 ModuleNotFoundError 

4341 If ``pywin32`` is not installed. 

4342 """ 

4343 if client is None: 

4344 raise ModuleNotFoundError( 

4345 "Could not import win32com.client. Run " 

4346 "`python -m pip install pywin32` on a Windows operating system " 

4347 "to install this package." 

4348 ) 

4349 

4350 is_dispatch = False 

4351 try: 

4352 is_dispatch = isinstance(obj_or_path, client.CDispatch) 

4353 except Exception: 

4354 is_dispatch = False 

4355 

4356 if is_dispatch or hasattr(obj_or_path, "Infos"): 

4357 self._com = obj_or_path 

4358 else: 

4359 self._com = client.Dispatch("Polyfile.Polyfile") 

4360 self._com.Open(str(obj_or_path)) 

4361 

4362 def __repr__(self) -> str: 

4363 """Return a debug representation.""" 

4364 return f"PolyFile({self.file_name!r})" 

4365 

4366 @property 

4367 def contained_files(self) -> list["PolyFile"]: 

4368 """Contained files if this file is a combined file. 

4369 

4370 Raises 

4371 ------ 

4372 ValueError 

4373 If the file contains no embedded files. 

4374 """ 

4375 try: 

4376 count = int(self._com.ContainedFiles.Count) 

4377 except Exception: 

4378 count = len(self._com.ContainedFiles) 

4379 

4380 if count == 0: 

4381 raise ValueError("The present file contains no contained files.") 

4382 return [PolyFile(obj.File) for obj in self._com.ContainedFiles] 

4383 

4384 @property 

4385 def parent_file(self) -> "PolyFile": 

4386 """Parent combined file containing this file. 

4387 

4388 Raises 

4389 ------ 

4390 ValueError 

4391 If this file has no parent combined file. 

4392 """ 

4393 try: 

4394 return PolyFile(self._com.CombinedFile) 

4395 except com_error as e: 

4396 raise ValueError("The present file contains no parent.") from e 

4397 

4398 @property 

4399 def title(self) -> str: 

4400 """Document title.""" 

4401 return self._com.SummaryInfo.Title 

4402 

4403 @property 

4404 def comments(self) -> str: 

4405 """Document comments.""" 

4406 return self._com.SummaryInfo.Comments 

4407 

4408 @property 

4409 def author(self) -> str: 

4410 """Document author.""" 

4411 return self._com.SummaryInfo.Author 

4412 

4413 @property 

4414 def subject(self) -> str: 

4415 """Document subject.""" 

4416 return self._com.SummaryInfo.Subject 

4417 

4418 @property 

4419 def date(self) -> Any: 

4420 """File timestamp as returned by the COM interface.""" 

4421 return self._com.Version.FileTime 

4422 

4423 @property 

4424 def version(self) -> dict[str, Any]: 

4425 """Version metadata reported by the COM interface.""" 

4426 keys = [ 

4427 "FileID", 

4428 "FileTime", 

4429 "FileVersion", 

4430 "ProgramID", 

4431 "ProgramVersion", 

4432 "ProgramVersionBuild", 

4433 "ProgramVersionMajor", 

4434 "ProgramVersionMinor", 

4435 "ProgramVersionRevision", 

4436 "ProgramVersionString", 

4437 ] 

4438 return {key: getattr(self._com.Version, key) for key in keys} 

4439 

4440 @property 

4441 def file_name(self) -> str: 

4442 """Path of the opened file.""" 

4443 return self._com.FileName 

4444 

4445 @property 

4446 def acquisition_mode(self) -> AcquisitionMode: 

4447 """Active acquisition mode for the file.""" 

4448 return AcquisitionMode(self._com.Infos.AcquisitionInfoModes.ActiveMode) 

4449 

4450 @property 

4451 def acquisition_properties(self) -> dict[str, Any]: 

4452 """Available acquisition-property wrappers for the active mode. 

4453 

4454 Returns 

4455 ------- 

4456 dict[str, Any] 

4457 Mapping from snake_case property names to wrapper objects. Singular 

4458 COM properties are returned as wrapper instances; collection-valued 

4459 COM properties are returned as tuples of wrapper instances. 

4460 """ 

4461 props = self._com.Infos.AcquisitionInfoModes.ActiveProperties 

4462 

4463 spec = [ 

4464 ("average", "HasAverageProperties", "AverageProperties", AverageProperties, True), 

4465 ("channels", "HasChannelsProperties", "ChannelsProperties", ChannelsProperties, False), 

4466 ( 

4467 "fast_scans", 

4468 "HasFastScansProperties", 

4469 "FastScansProperties", 

4470 FastScanProperties, 

4471 True, 

4472 ), 

4473 ("fft", "HasFftProperties", "FftProperties", FFTProperties, True), 

4474 ("front_end", "HasFrontEndProperties", "FrontEndProperties", FrontEndProperties, True), 

4475 ("general", "HasGeneralProperties", "GeneralProperties", GeneralProperties, True), 

4476 ( 

4477 "generators", 

4478 "HasGeneratorsProperties", 

4479 "GeneratorsProperties", 

4480 GeneratorsProperties, 

4481 False, 

4482 ), 

4483 ( 

4484 "multi_frame", 

4485 "HasMultiFrameProperties", 

4486 "MultiFrameProperties", 

4487 MultiFrameProperties, 

4488 True, 

4489 ), 

4490 ( 

4491 "signal_enhancement", 

4492 "HasSignalEnhancementProperties", 

4493 "SignalEnhancementProperties", 

4494 SignalEnhancementProperties, 

4495 True, 

4496 ), 

4497 ("time", "HasTimeProperties", "TimeProperties", TimeProperties, True), 

4498 ("trigger", "HasTriggerProperties", "TriggerProperties", TriggerProperties, True), 

4499 ( 

4500 "vibrometers", 

4501 "HasVibrometersProperties", 

4502 "VibrometersProperties", 

4503 VibrometerProperties, 

4504 False, 

4505 ), 

4506 ("zoom_fft", "HasZoomFftProperties", "ZoomFftProperties", ZoomFFTProperties, True), 

4507 ] 

4508 

4509 out: dict[str, Any] = {} 

4510 for key, has_attr, com_attr, wrapper_cls, scalar in spec: 

4511 if getattr(props, has_attr): 

4512 com_value = getattr(props, com_attr) 

4513 out[key] = ( 

4514 wrapper_cls(com_value) if scalar else tuple(wrapper_cls(x) for x in com_value) 

4515 ) 

4516 return out 

4517 

4518 @property 

4519 def alignments(self) -> Alignments: 

4520 """Alignment information.""" 

4521 return Alignments(self._com.Infos.Alignments) 

4522 

4523 @property 

4524 def camera_settings(self) -> None: 

4525 """Camera settings. 

4526 

4527 Raises 

4528 ------ 

4529 NotImplementedError 

4530 The COM object returned by this property is undocumented and is not 

4531 wrapped by this module. 

4532 """ 

4533 raise NotImplementedError( 

4534 "CameraSettings returns an ICamera object which is for internal use " 

4535 "only and not documented." 

4536 ) 

4537 

4538 @property 

4539 def elements(self) -> list[Element]: 

4540 """Geometry elements.""" 

4541 return [Element(val) for val in self._com.Infos.Elements] 

4542 

4543 @property 

4544 def hardware(self) -> Hardware: 

4545 """Hardware information.""" 

4546 return Hardware(self._com.Infos.Hardware) 

4547 

4548 @property 

4549 def measurement_points(self) -> MeasPoints: 

4550 """Measurement points.""" 

4551 return MeasPoints(self._com.Infos.MeasPoints) 

4552 

4553 @property 

4554 def measurement_locations(self) -> list[MeasurementLocation]: 

4555 """Measurement locations. 

4556 

4557 Raises 

4558 ------ 

4559 ValueError 

4560 If no measurement locations are defined in the dataset. 

4561 """ 

4562 try: 

4563 return [MeasurementLocation(v) for v in self._com.Infos.MeasurementLocations] 

4564 except com_error as e: 

4565 raise ValueError( 

4566 "No measurement locations defined in this dataset. Did you mean " 

4567 "`measurement_points`?" 

4568 ) from e 

4569 

4570 @property 

4571 def profiles(self) -> list[Profile]: 

4572 """Profiles defined in the dataset. 

4573 

4574 Raises 

4575 ------ 

4576 ValueError 

4577 If no profiles are defined in the dataset. 

4578 """ 

4579 try: 

4580 return [Profile(v) for v in self._com.Infos.Profiles] 

4581 except com_error as e: 

4582 raise ValueError("No profiles defined in this dataset.") from e 

4583 

4584 @property 

4585 def scan_head_devices_info(self) -> ScanHeadDevicesInfo: 

4586 """Scan-head-device information.""" 

4587 return ScanHeadDevicesInfo(self._com.Infos.ScanHeadDevicesInfo) 

4588 

4589 @property 

4590 def spectrogram_info(self) -> SpectrogramInfo: 

4591 """Spectrogram information.""" 

4592 return SpectrogramInfo(self._com.Infos.SpectrogramInfo) 

4593 

4594 @property 

4595 def textures(self) -> list[Texture]: 

4596 """Textures associated with the dataset.""" 

4597 return [Texture(v) for v in self._com.Infos.Textures] 

4598 

4599 @property 

4600 def video_bitmap(self) -> VideoBitmap: 

4601 """Video bitmap information.""" 

4602 return VideoBitmap(self._com.Infos.VideoBitmap) 

4603 

4604 @property 

4605 def video_mapping_info(self) -> VideoMappingInfo: 

4606 """Video mapping information.""" 

4607 return VideoMappingInfo(self._com.Infos.VideoMappingInfo) 

4608 

4609 @property 

4610 def point_domains(self) -> list[PointDomain]: 

4611 """Available point domains.""" 

4612 return [PointDomain(v) for v in self._com.GetPointDomains()] 

4613 

4614 def _get_single_point_domain(self, domain_type: DomainType) -> PointDomain: 

4615 """Return the single point domain of the requested type. 

4616 

4617 Raises 

4618 ------ 

4619 ValueError 

4620 If zero or more than one domains of the requested type are found. 

4621 """ 

4622 domains = [domain for domain in self.point_domains if domain.type == domain_type] 

4623 if len(domains) > 1: 

4624 raise ValueError(f"Found more than one domain with type {domain_type.name}") 

4625 if len(domains) == 0: 

4626 raise ValueError(f"No data found with domain type {domain_type.name}") 

4627 return domains[0] 

4628 

4629 @staticmethod 

4630 def _channel_is_reference(channel: Channel) -> bool: 

4631 """Heuristic check for whether a channel is a reference channel. 

4632 

4633 Notes 

4634 ----- 

4635 This currently preserves the original module behavior by using the 

4636 channel name. If a more reliable COM property becomes available, this 

4637 method is the preferred place to update the logic. 

4638 """ 

4639 return "vib" not in channel.name.lower() 

4640 

4641 @staticmethod 

4642 def _single_response_dof( 

4643 description: SignalDescription, channel_name: str, signal_name: str 

4644 ) -> DegreeOfFreedomID: 

4645 """Return the single response DOF for a signal description.""" 

4646 if len(description.response_dofs) == 1: 

4647 return description.response_dofs[0] 

4648 raise NotImplementedError( 

4649 f"Multiple Response DoFs were found for this signal " 

4650 f"({channel_name} {signal_name}). Not sure how to proceed." 

4651 ) 

4652 

4653 @staticmethod 

4654 def _single_reference_dof( 

4655 description: SignalDescription, channel_name: str, signal_name: str 

4656 ) -> DegreeOfFreedomID: 

4657 """Return the single reference DOF for a signal description.""" 

4658 if len(description.reference_dofs) == 1: 

4659 return description.reference_dofs[0] 

4660 raise NotImplementedError( 

4661 f"Multiple Reference DoFs were found for this signal " 

4662 f"({channel_name} {signal_name}). Not sure how to proceed." 

4663 ) 

4664 

4665 @staticmethod 

4666 def _single_response_or_reference_dof( 

4667 description: SignalDescription, channel_name: str, signal_name: str 

4668 ) -> DegreeOfFreedomID: 

4669 """Return a single response DOF, or fall back to a single reference DOF.""" 

4670 if len(description.response_dofs) == 1: 

4671 return description.response_dofs[0] 

4672 if len(description.reference_dofs) == 1: 

4673 return description.reference_dofs[0] 

4674 raise NotImplementedError( 

4675 f"Neither a single response DOF nor a single reference DOF could be " 

4676 f"determined for signal ({channel_name} {signal_name})." 

4677 ) 

4678 

4679 @staticmethod 

4680 def _measpoint_coordinate_for_direction(data_point: DataPoint, direction_value: int) -> Any: 

4681 """Build a coordinate-array entry for a measurement point.""" 

4682 return coordinate_array(data_point.meas_point.label, direction_value) 

4683 

4684 @staticmethod 

4685 def _append_comments( 

4686 comment1s: list[str], 

4687 comment2s: list[str], 

4688 comment3s: list[str], 

4689 comment4s: list[str], 

4690 comment5s: list[str], 

4691 *, 

4692 unit: str, 

4693 file_name: str, 

4694 domain_name: str, 

4695 channel_name: str, 

4696 signal_name: str, 

4697 ) -> None: 

4698 """Append the standard comment fields used for sdynpy outputs.""" 

4699 comment1s.append(unit) 

4700 comment2s.append(file_name[-80:]) 

4701 comment3s.append(domain_name) 

4702 comment4s.append(channel_name) 

4703 comment5s.append(signal_name) 

4704 

4705 def get_transfer_functions( 

4706 self, 

4707 method: str = "h1", 

4708 data_type: str = "acceleration", 

4709 collapse_reference_signals: bool = True, 

4710 ) -> TransferFunctionArray: 

4711 """Extract transfer functions from the active spectrum point domain. 

4712 

4713 Parameters 

4714 ---------- 

4715 method 

4716 Transfer-function estimator to extract. Supported values are 

4717 ``"h1"`` and ``"h2"``. 

4718 data_type 

4719 Response quantity to extract from the signal name. Supported values 

4720 are ``"acceleration"``, ``"velocity"``, and ``"displacement"``. 

4721 collapse_reference_signals 

4722 If ``True``, signals identified as reference-channel signals are 

4723 averaged across measurement points and returned as a single trace 

4724 per signal. If ``False``, all reference-channel traces are returned 

4725 individually. 

4726 

4727 Returns 

4728 ------- 

4729 TransferFunctionArray 

4730 An ``sdynpy`` transfer-function array produced by 

4731 :func:`transfer_function_array`. 

4732 """ 

4733 method_l = method.lower() 

4734 data_type_l = data_type.lower() 

4735 

4736 if method_l not in {"h1", "h2"}: 

4737 raise ValueError('`method` argument must be one of "h1" or "h2"') 

4738 if data_type_l not in {"acceleration", "velocity", "displacement"}: 

4739 raise ValueError( 

4740 '`data_type` argument must be one of "acceleration", ' '"velocity", "displacement"' 

4741 ) 

4742 

4743 domain = self._get_single_point_domain(DomainType.SPECTRUM) 

4744 data_points = domain.data_points 

4745 

4746 abscissas: list[np.ndarray] = [] 

4747 response_coords: list[Any] = [] 

4748 reference_coords: list[Any] = [] 

4749 ordinates: list[np.ndarray] = [] 

4750 comment1s: list[str] = [] 

4751 comment2s: list[str] = [] 

4752 comment3s: list[str] = [] 

4753 comment4s: list[str] = [] 

4754 comment5s: list[str] = [] 

4755 

4756 for channel in domain.channels: 

4757 if VERBOSE: 

4758 print(f"Looking at Channel {channel.name}") 

4759 

4760 for signal in channel.signals: 

4761 if VERBOSE: 

4762 print(f" Looking at Signal {signal.name}") 

4763 

4764 signal_name_l = signal.name.lower() 

4765 if method_l not in signal_name_l or data_type_l not in signal_name_l: 

4766 continue 

4767 

4768 if VERBOSE: 

4769 print(" This signal is an FRF") 

4770 

4771 display = _find_display_by_name(signal, "Real & Imag.") 

4772 description = signal.description 

4773 x_axis = description.x_axis 

4774 abscissa = _build_linear_abscissa(x_axis) 

4775 y_axis = description.y_axis 

4776 

4777 response_dof = self._single_response_dof(description, channel.name, signal.name) 

4778 reference_dof = self._single_reference_dof(description, channel.name, signal.name) 

4779 

4780 response_coord = coordinate_array(response_dof.node, response_dof.direction.value) 

4781 reference_coord = coordinate_array( 

4782 reference_dof.node, reference_dof.direction.value 

4783 ) 

4784 

4785 if VERBOSE: 

4786 print(" Degrees of Freedom") 

4787 print(f" Response: {response_coord}") 

4788 print(f" Reference: {reference_coord}") 

4789 

4790 this_ordinate: list[np.ndarray] = [] 

4791 this_measpoint_coords: list[Any] = [] 

4792 

4793 for data_point in data_points: 

4794 measpoint_coord = self._measpoint_coordinate_for_direction( 

4795 data_point, response_coord.direction 

4796 ) 

4797 this_measpoint_coords.append(measpoint_coord) 

4798 data_array = data_point.get_data(display) 

4799 ordinate = data_array[::2] + 1j * data_array[1::2] 

4800 this_ordinate.append(ordinate) 

4801 

4802 this_ordinate_arr = np.asarray(this_ordinate) 

4803 

4804 if self._channel_is_reference(channel): 

4805 if VERBOSE: 

4806 print(" This is a reference channel.") 

4807 if collapse_reference_signals: 

4808 if VERBOSE: 

4809 print(" Collapsing Reference Signals to a single signal") 

4810 ordinates.append(np.mean(this_ordinate_arr, axis=0, keepdims=True)) 

4811 abscissas.append(abscissa[np.newaxis]) 

4812 response_coords.append(response_coord[np.newaxis]) 

4813 reference_coords.append(reference_coord[np.newaxis]) 

4814 self._append_comments( 

4815 comment1s, 

4816 comment2s, 

4817 comment3s, 

4818 comment4s, 

4819 comment5s, 

4820 unit=y_axis.unit, 

4821 file_name=self.file_name, 

4822 domain_name=domain.name, 

4823 channel_name=channel.name, 

4824 signal_name=signal.name, 

4825 ) 

4826 else: 

4827 if VERBOSE: 

4828 print(" Keeping all reference signals") 

4829 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

4830 ordinates.append(ordinate[np.newaxis]) 

4831 abscissas.append(abscissa[np.newaxis]) 

4832 response_coords.append(response_coord[np.newaxis]) 

4833 reference_coords.append(reference_coord[np.newaxis]) 

4834 self._append_comments( 

4835 comment1s, 

4836 comment2s, 

4837 comment3s, 

4838 comment4s, 

4839 comment5s, 

4840 unit=y_axis.unit, 

4841 file_name=self.file_name, 

4842 domain_name=domain.name, 

4843 channel_name=f"{channel.name} at Measurement Point: {coord.node}", 

4844 signal_name=signal.name, 

4845 ) 

4846 else: 

4847 if VERBOSE: 

4848 print(" This is not a reference channel.") 

4849 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

4850 ordinates.append(ordinate[np.newaxis]) 

4851 abscissas.append(abscissa[np.newaxis]) 

4852 response_coords.append(coord[np.newaxis]) 

4853 reference_coords.append(reference_coord[np.newaxis]) 

4854 self._append_comments( 

4855 comment1s, 

4856 comment2s, 

4857 comment3s, 

4858 comment4s, 

4859 comment5s, 

4860 unit=y_axis.unit, 

4861 file_name=self.file_name, 

4862 domain_name=domain.name, 

4863 channel_name=channel.name, 

4864 signal_name=signal.name, 

4865 ) 

4866 

4867 if not ordinates: 

4868 raise ValueError( 

4869 f"No matching transfer-function signals were found for " 

4870 f"method={method!r}, data_type={data_type!r}." 

4871 ) 

4872 

4873 ordinate = np.concatenate(ordinates) 

4874 abscissa = np.concatenate(abscissas) 

4875 coordinate = np.concatenate( 

4876 ( 

4877 np.concatenate(response_coords)[..., np.newaxis], 

4878 np.concatenate(reference_coords)[..., np.newaxis], 

4879 ), 

4880 axis=-1, 

4881 ) 

4882 return transfer_function_array( 

4883 abscissa, 

4884 ordinate, 

4885 coordinate, 

4886 comment1s, 

4887 comment2s, 

4888 comment3s, 

4889 comment4s, 

4890 comment5s, 

4891 ) 

4892 

4893 def get_coherence(self, collapse_reference_signals: bool = True) -> CoherenceArray: 

4894 """Extract coherence functions from the active spectrum point domain. 

4895 

4896 Parameters 

4897 ---------- 

4898 collapse_reference_signals 

4899 If ``True``, signals identified as reference-channel signals are 

4900 averaged across measurement points and returned as a single trace 

4901 per signal. If ``False``, all traces are returned individually. 

4902 

4903 Returns 

4904 ------- 

4905 CoherenceArray 

4906 An ``sdynpy`` coherence array produced by :func:`coherence_array`. 

4907 """ 

4908 domain = self._get_single_point_domain(DomainType.SPECTRUM) 

4909 data_points = domain.data_points 

4910 

4911 abscissas: list[np.ndarray] = [] 

4912 response_coords: list[Any] = [] 

4913 reference_coords: list[Any] = [] 

4914 ordinates: list[np.ndarray] = [] 

4915 comment1s: list[str] = [] 

4916 comment2s: list[str] = [] 

4917 comment3s: list[str] = [] 

4918 comment4s: list[str] = [] 

4919 comment5s: list[str] = [] 

4920 

4921 for channel in domain.channels: 

4922 if VERBOSE: 

4923 print(f"Looking at Channel {channel.name}") 

4924 

4925 for signal in channel.signals: 

4926 if VERBOSE: 

4927 print(f" Looking at Signal {signal.name}") 

4928 

4929 if signal.name.lower() != "coherence": 

4930 continue 

4931 

4932 if VERBOSE: 

4933 print(" This signal is coherence") 

4934 

4935 display = _find_display_by_name(signal, "Magnitude") 

4936 description = signal.description 

4937 x_axis = description.x_axis 

4938 abscissa = _build_linear_abscissa(x_axis) 

4939 y_axis = description.y_axis 

4940 

4941 response_dof = self._single_response_dof(description, channel.name, signal.name) 

4942 reference_dof = self._single_reference_dof(description, channel.name, signal.name) 

4943 

4944 response_coord = coordinate_array(response_dof.node, response_dof.direction.value) 

4945 reference_coord = coordinate_array( 

4946 reference_dof.node, reference_dof.direction.value 

4947 ) 

4948 

4949 if VERBOSE: 

4950 print(" Degrees of Freedom") 

4951 print(f" Response: {response_coord}") 

4952 print(f" Reference: {reference_coord}") 

4953 

4954 this_ordinate: list[np.ndarray] = [] 

4955 this_measpoint_coords: list[Any] = [] 

4956 

4957 for data_point in data_points: 

4958 measpoint_coord = self._measpoint_coordinate_for_direction( 

4959 data_point, response_coord.direction 

4960 ) 

4961 this_measpoint_coords.append(measpoint_coord) 

4962 ordinate = data_point.get_data(display) 

4963 this_ordinate.append(ordinate) 

4964 

4965 this_ordinate_arr = np.asarray(this_ordinate) 

4966 

4967 if self._channel_is_reference(channel): 

4968 if VERBOSE: 

4969 print(" This is a reference channel.") 

4970 if collapse_reference_signals: 

4971 if VERBOSE: 

4972 print(" Collapsing Reference Signals to a single signal") 

4973 ordinates.append(np.mean(this_ordinate_arr, axis=0, keepdims=True)) 

4974 abscissas.append(abscissa[np.newaxis]) 

4975 response_coords.append(response_coord[np.newaxis]) 

4976 reference_coords.append(reference_coord[np.newaxis]) 

4977 self._append_comments( 

4978 comment1s, 

4979 comment2s, 

4980 comment3s, 

4981 comment4s, 

4982 comment5s, 

4983 unit=y_axis.unit, 

4984 file_name=self.file_name, 

4985 domain_name=domain.name, 

4986 channel_name=channel.name, 

4987 signal_name=signal.name, 

4988 ) 

4989 else: 

4990 if VERBOSE: 

4991 print(" Keeping all reference signals") 

4992 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

4993 ordinates.append(ordinate[np.newaxis]) 

4994 abscissas.append(abscissa[np.newaxis]) 

4995 response_coords.append(response_coord[np.newaxis]) 

4996 reference_coords.append(reference_coord[np.newaxis]) 

4997 self._append_comments( 

4998 comment1s, 

4999 comment2s, 

5000 comment3s, 

5001 comment4s, 

5002 comment5s, 

5003 unit=y_axis.unit, 

5004 file_name=self.file_name, 

5005 domain_name=domain.name, 

5006 channel_name=f"{channel.name} at Measurement Point: {coord.node}", 

5007 signal_name=signal.name, 

5008 ) 

5009 else: 

5010 if VERBOSE: 

5011 print(" This is not a reference channel.") 

5012 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

5013 ordinates.append(ordinate[np.newaxis]) 

5014 abscissas.append(abscissa[np.newaxis]) 

5015 response_coords.append(coord[np.newaxis]) 

5016 reference_coords.append(reference_coord[np.newaxis]) 

5017 self._append_comments( 

5018 comment1s, 

5019 comment2s, 

5020 comment3s, 

5021 comment4s, 

5022 comment5s, 

5023 unit=y_axis.unit, 

5024 file_name=self.file_name, 

5025 domain_name=domain.name, 

5026 channel_name=channel.name, 

5027 signal_name=signal.name, 

5028 ) 

5029 

5030 if not ordinates: 

5031 raise ValueError("No coherence signals found in this dataset.") 

5032 

5033 ordinate = np.concatenate(ordinates) 

5034 abscissa = np.concatenate(abscissas) 

5035 coordinate = np.concatenate( 

5036 ( 

5037 np.concatenate(response_coords)[..., np.newaxis], 

5038 np.concatenate(reference_coords)[..., np.newaxis], 

5039 ), 

5040 axis=-1, 

5041 ) 

5042 return coherence_array( 

5043 abscissa, 

5044 ordinate, 

5045 coordinate, 

5046 comment1s, 

5047 comment2s, 

5048 comment3s, 

5049 comment4s, 

5050 comment5s, 

5051 ) 

5052 

5053 def get_multiple_coherence( 

5054 self, collapse_reference_signals: bool = True 

5055 ) -> MultipleCoherenceArray: 

5056 """Extract multiple-coherence functions from the active spectrum point domain. 

5057 

5058 Parameters 

5059 ---------- 

5060 collapse_reference_signals 

5061 If ``True``, signals identified as reference-channel signals are 

5062 averaged across measurement points and returned as a single trace 

5063 per signal. If ``False``, all traces are returned individually. 

5064 

5065 Returns 

5066 ------- 

5067 MultipleCoherenceArray 

5068 An ``sdynpy`` multiple-coherence array produced by 

5069 :func:`multiple_coherence_array`. 

5070 """ 

5071 domain = self._get_single_point_domain(DomainType.SPECTRUM) 

5072 data_points = domain.data_points 

5073 

5074 abscissas: list[np.ndarray] = [] 

5075 response_coords: list[Any] = [] 

5076 ordinates: list[np.ndarray] = [] 

5077 comment1s: list[str] = [] 

5078 comment2s: list[str] = [] 

5079 comment3s: list[str] = [] 

5080 comment4s: list[str] = [] 

5081 comment5s: list[str] = [] 

5082 

5083 for channel in domain.channels: 

5084 if VERBOSE: 

5085 print(f"Looking at Channel {channel.name}") 

5086 

5087 for signal in channel.signals: 

5088 if VERBOSE: 

5089 print(f" Looking at Signal {signal.name}") 

5090 

5091 if signal.name.lower() != "multiple coherence": 

5092 continue 

5093 

5094 if VERBOSE: 

5095 print(" This signal is multiple coherence") 

5096 

5097 display = _find_display_by_name(signal, "Magnitude") 

5098 description = signal.description 

5099 x_axis = description.x_axis 

5100 abscissa = _build_linear_abscissa(x_axis) 

5101 y_axis = description.y_axis 

5102 

5103 response_dof = self._single_response_dof(description, channel.name, signal.name) 

5104 response_coord = coordinate_array(response_dof.node, response_dof.direction.value) 

5105 

5106 if VERBOSE: 

5107 print(" Degrees of Freedom") 

5108 print(f" Response: {response_coord}") 

5109 

5110 this_ordinate: list[np.ndarray] = [] 

5111 this_measpoint_coords: list[Any] = [] 

5112 

5113 for data_point in data_points: 

5114 measpoint_coord = self._measpoint_coordinate_for_direction( 

5115 data_point, response_coord.direction 

5116 ) 

5117 this_measpoint_coords.append(measpoint_coord) 

5118 ordinate = data_point.get_data(display) 

5119 this_ordinate.append(ordinate) 

5120 

5121 this_ordinate_arr = np.asarray(this_ordinate) 

5122 

5123 if self._channel_is_reference(channel): 

5124 if VERBOSE: 

5125 print(" This is a reference channel.") 

5126 if collapse_reference_signals: 

5127 if VERBOSE: 

5128 print(" Collapsing Reference Signals to a single signal") 

5129 ordinates.append(np.mean(this_ordinate_arr, axis=0, keepdims=True)) 

5130 abscissas.append(abscissa[np.newaxis]) 

5131 response_coords.append(response_coord[np.newaxis]) 

5132 self._append_comments( 

5133 comment1s, 

5134 comment2s, 

5135 comment3s, 

5136 comment4s, 

5137 comment5s, 

5138 unit=y_axis.unit, 

5139 file_name=self.file_name, 

5140 domain_name=domain.name, 

5141 channel_name=channel.name, 

5142 signal_name=signal.name, 

5143 ) 

5144 else: 

5145 if VERBOSE: 

5146 print(" Keeping all reference signals") 

5147 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

5148 ordinates.append(ordinate[np.newaxis]) 

5149 abscissas.append(abscissa[np.newaxis]) 

5150 response_coords.append(response_coord[np.newaxis]) 

5151 self._append_comments( 

5152 comment1s, 

5153 comment2s, 

5154 comment3s, 

5155 comment4s, 

5156 comment5s, 

5157 unit=y_axis.unit, 

5158 file_name=self.file_name, 

5159 domain_name=domain.name, 

5160 channel_name=f"{channel.name} at Measurement Point: {coord.node}", 

5161 signal_name=signal.name, 

5162 ) 

5163 else: 

5164 if VERBOSE: 

5165 print(" This is not a reference channel.") 

5166 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

5167 ordinates.append(ordinate[np.newaxis]) 

5168 abscissas.append(abscissa[np.newaxis]) 

5169 response_coords.append(coord[np.newaxis]) 

5170 self._append_comments( 

5171 comment1s, 

5172 comment2s, 

5173 comment3s, 

5174 comment4s, 

5175 comment5s, 

5176 unit=y_axis.unit, 

5177 file_name=self.file_name, 

5178 domain_name=domain.name, 

5179 channel_name=channel.name, 

5180 signal_name=signal.name, 

5181 ) 

5182 

5183 if not ordinates: 

5184 raise ValueError("No multiple coherence signals found in this dataset.") 

5185 

5186 ordinate = np.concatenate(ordinates) 

5187 abscissa = np.concatenate(abscissas) 

5188 coordinate = np.concatenate(response_coords)[..., np.newaxis] 

5189 return multiple_coherence_array( 

5190 abscissa, 

5191 ordinate, 

5192 coordinate, 

5193 comment1s, 

5194 comment2s, 

5195 comment3s, 

5196 comment4s, 

5197 comment5s, 

5198 ) 

5199 

5200 def get_power_spectral_densities( 

5201 self, 

5202 data_type: str = "acceleration", 

5203 collapse_reference_signals: bool = True, 

5204 ) -> PowerSpectralDensityArray: 

5205 """Extract power spectral densities from the active spectrum point domain. 

5206 

5207 Parameters 

5208 ---------- 

5209 data_type 

5210 Preferred quantity to extract from the signal name. Supported values 

5211 are ``"displacement"``, ``"velocity"``, and ``"acceleration"``. 

5212 collapse_reference_signals 

5213 If ``True``, signals identified as reference-channel signals are 

5214 averaged across measurement points and returned as a single trace 

5215 per signal. If ``False``, all traces are returned individually. 

5216 

5217 Returns 

5218 ------- 

5219 PowerSpectralDensityArray 

5220 An ``sdynpy`` PSD array produced by 

5221 :func:`power_spectral_density_array`. 

5222 """ 

5223 data_type_l = data_type.lower() 

5224 potential_data_types = {"displacement", "velocity", "acceleration"} 

5225 if data_type_l not in potential_data_types: 

5226 raise ValueError( 

5227 '`data_type` argument must be one of "acceleration", "velocity", "displacement"' 

5228 ) 

5229 

5230 domain = self._get_single_point_domain(DomainType.SPECTRUM) 

5231 data_points = domain.data_points 

5232 

5233 abscissas: list[np.ndarray] = [] 

5234 response_coords: list[Any] = [] 

5235 reference_coords: list[Any] = [] 

5236 ordinates: list[np.ndarray] = [] 

5237 comment1s: list[str] = [] 

5238 comment2s: list[str] = [] 

5239 comment3s: list[str] = [] 

5240 comment4s: list[str] = [] 

5241 comment5s: list[str] = [] 

5242 

5243 for channel in domain.channels: 

5244 if VERBOSE: 

5245 print(f"Looking at Channel {channel.name}") 

5246 

5247 for signal in channel.signals: 

5248 if VERBOSE: 

5249 print(f" Looking at Signal {signal.name}") 

5250 

5251 signal_name_l = signal.name.lower() 

5252 if not ( 

5253 "psd" in signal_name_l 

5254 and ( 

5255 data_type_l in signal_name_l 

5256 or not any(dt in signal_name_l for dt in potential_data_types) 

5257 ) 

5258 ): 

5259 continue 

5260 

5261 if VERBOSE: 

5262 print(" This signal is a PSD") 

5263 

5264 display = _find_display_by_name(signal, "Magnitude") 

5265 description = signal.description 

5266 x_axis = description.x_axis 

5267 abscissa = _build_linear_abscissa(x_axis) 

5268 y_axis = description.y_axis 

5269 

5270 response_dof = self._single_response_or_reference_dof( 

5271 description, channel.name, signal.name 

5272 ) 

5273 response_coord = coordinate_array(response_dof.node, response_dof.direction.value) 

5274 reference_coord = response_coord 

5275 

5276 if VERBOSE: 

5277 print(" Degrees of Freedom") 

5278 print(f" Response: {response_coord}") 

5279 print(f" Reference: {reference_coord}") 

5280 

5281 this_ordinate: list[np.ndarray] = [] 

5282 this_measpoint_coords: list[Any] = [] 

5283 

5284 for data_point in data_points: 

5285 measpoint_coord = self._measpoint_coordinate_for_direction( 

5286 data_point, response_coord.direction 

5287 ) 

5288 this_measpoint_coords.append(measpoint_coord) 

5289 ordinate = data_point.get_data(display) 

5290 this_ordinate.append(ordinate) 

5291 

5292 this_ordinate_arr = np.asarray(this_ordinate) 

5293 

5294 if self._channel_is_reference(channel): 

5295 if VERBOSE: 

5296 print(" This is a reference channel.") 

5297 if collapse_reference_signals: 

5298 if VERBOSE: 

5299 print(" Collapsing Reference Signals to a single signal") 

5300 ordinates.append(np.mean(this_ordinate_arr, axis=0, keepdims=True)) 

5301 abscissas.append(abscissa[np.newaxis]) 

5302 response_coords.append(response_coord[np.newaxis]) 

5303 reference_coords.append(reference_coord[np.newaxis]) 

5304 self._append_comments( 

5305 comment1s, 

5306 comment2s, 

5307 comment3s, 

5308 comment4s, 

5309 comment5s, 

5310 unit=y_axis.unit, 

5311 file_name=self.file_name, 

5312 domain_name=domain.name, 

5313 channel_name=channel.name, 

5314 signal_name=signal.name, 

5315 ) 

5316 else: 

5317 if VERBOSE: 

5318 print(" Keeping all reference signals") 

5319 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

5320 ordinates.append(ordinate[np.newaxis]) 

5321 abscissas.append(abscissa[np.newaxis]) 

5322 response_coords.append(response_coord[np.newaxis]) 

5323 reference_coords.append(reference_coord[np.newaxis]) 

5324 self._append_comments( 

5325 comment1s, 

5326 comment2s, 

5327 comment3s, 

5328 comment4s, 

5329 comment5s, 

5330 unit=y_axis.unit, 

5331 file_name=self.file_name, 

5332 domain_name=domain.name, 

5333 channel_name=f"{channel.name} at Measurement Point: {coord.node}", 

5334 signal_name=signal.name, 

5335 ) 

5336 else: 

5337 if VERBOSE: 

5338 print(" This is not a reference channel.") 

5339 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

5340 ordinates.append(ordinate[np.newaxis]) 

5341 abscissas.append(abscissa[np.newaxis]) 

5342 response_coords.append(coord[np.newaxis]) 

5343 reference_coords.append(coord[np.newaxis]) 

5344 self._append_comments( 

5345 comment1s, 

5346 comment2s, 

5347 comment3s, 

5348 comment4s, 

5349 comment5s, 

5350 unit=y_axis.unit, 

5351 file_name=self.file_name, 

5352 domain_name=domain.name, 

5353 channel_name=channel.name, 

5354 signal_name=signal.name, 

5355 ) 

5356 

5357 if not ordinates: 

5358 raise ValueError("No PSD signals found in this dataset.") 

5359 

5360 ordinate = np.concatenate(ordinates) 

5361 abscissa = np.concatenate(abscissas) 

5362 coordinate = np.concatenate( 

5363 ( 

5364 np.concatenate(response_coords)[..., np.newaxis], 

5365 np.concatenate(reference_coords)[..., np.newaxis], 

5366 ), 

5367 axis=-1, 

5368 ) 

5369 return power_spectral_density_array( 

5370 abscissa, 

5371 ordinate, 

5372 coordinate, 

5373 comment1s, 

5374 comment2s, 

5375 comment3s, 

5376 comment4s, 

5377 comment5s, 

5378 ) 

5379 

5380 def get_time_response(self, collapse_reference_signals: bool = True) -> TimeHistoryArray: 

5381 """Extract time histories from the active time-domain point domain. 

5382 

5383 Parameters 

5384 ---------- 

5385 collapse_reference_signals 

5386 If ``True``, signals identified as reference-channel signals are 

5387 averaged across measurement points and returned as a single trace 

5388 per signal. If ``False``, all traces are returned individually. 

5389 

5390 Returns 

5391 ------- 

5392 TimeHistoryArray 

5393 An ``sdynpy`` time-history array produced by 

5394 :func:`time_history_array`. 

5395 """ 

5396 domain = self._get_single_point_domain(DomainType.TIME) 

5397 data_points = domain.data_points 

5398 

5399 abscissas: list[np.ndarray] = [] 

5400 response_coords: list[Any] = [] 

5401 ordinates: list[np.ndarray] = [] 

5402 comment1s: list[str] = [] 

5403 comment2s: list[str] = [] 

5404 comment3s: list[str] = [] 

5405 comment4s: list[str] = [] 

5406 comment5s: list[str] = [] 

5407 

5408 for channel in domain.channels: 

5409 if VERBOSE: 

5410 print(f"Looking at Channel {channel.name}") 

5411 

5412 for signal in channel.signals: 

5413 if VERBOSE: 

5414 print(f" Looking at Signal {signal.name}") 

5415 

5416 if "voltage" in signal.name.lower(): 

5417 continue 

5418 

5419 if VERBOSE: 

5420 print(" This signal is time data") 

5421 

5422 display = _find_display_by_name(signal, "Samples") 

5423 description = signal.description 

5424 x_axis = description.x_axis 

5425 abscissa = _build_linear_abscissa(x_axis) 

5426 y_axis = description.y_axis 

5427 

5428 response_dof = self._single_response_or_reference_dof( 

5429 description, channel.name, signal.name 

5430 ) 

5431 response_coord = coordinate_array(response_dof.node, response_dof.direction.value) 

5432 

5433 if VERBOSE: 

5434 print(" Degrees of Freedom") 

5435 print(f" Response: {response_coord}") 

5436 

5437 this_ordinate: list[np.ndarray] = [] 

5438 this_measpoint_coords: list[Any] = [] 

5439 

5440 for data_point in data_points: 

5441 measpoint_coord = self._measpoint_coordinate_for_direction( 

5442 data_point, response_coord.direction 

5443 ) 

5444 this_measpoint_coords.append(measpoint_coord) 

5445 ordinate = data_point.get_data(display) 

5446 this_ordinate.append(ordinate) 

5447 

5448 this_ordinate_arr = np.asarray(this_ordinate) 

5449 

5450 if self._channel_is_reference(channel): 

5451 if VERBOSE: 

5452 print(" This is a reference channel.") 

5453 if collapse_reference_signals: 

5454 if VERBOSE: 

5455 print(" Collapsing Reference Signals to a single signal") 

5456 ordinates.append(np.mean(this_ordinate_arr, axis=0, keepdims=True)) 

5457 abscissas.append(abscissa[np.newaxis]) 

5458 response_coords.append(response_coord[np.newaxis]) 

5459 self._append_comments( 

5460 comment1s, 

5461 comment2s, 

5462 comment3s, 

5463 comment4s, 

5464 comment5s, 

5465 unit=y_axis.unit, 

5466 file_name=self.file_name, 

5467 domain_name=domain.name, 

5468 channel_name=channel.name, 

5469 signal_name=signal.name, 

5470 ) 

5471 else: 

5472 if VERBOSE: 

5473 print(" Keeping all reference signals") 

5474 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

5475 ordinates.append(ordinate[np.newaxis]) 

5476 abscissas.append(abscissa[np.newaxis]) 

5477 response_coords.append(response_coord[np.newaxis]) 

5478 self._append_comments( 

5479 comment1s, 

5480 comment2s, 

5481 comment3s, 

5482 comment4s, 

5483 comment5s, 

5484 unit=y_axis.unit, 

5485 file_name=self.file_name, 

5486 domain_name=domain.name, 

5487 channel_name=f"{channel.name} at Measurement Point: {coord.node}", 

5488 signal_name=signal.name, 

5489 ) 

5490 else: 

5491 if VERBOSE: 

5492 print(" This is not a reference channel.") 

5493 for ordinate, coord in zip(this_ordinate_arr, this_measpoint_coords): 

5494 ordinates.append(ordinate[np.newaxis]) 

5495 abscissas.append(abscissa[np.newaxis]) 

5496 response_coords.append(coord[np.newaxis]) 

5497 self._append_comments( 

5498 comment1s, 

5499 comment2s, 

5500 comment3s, 

5501 comment4s, 

5502 comment5s, 

5503 unit=y_axis.unit, 

5504 file_name=self.file_name, 

5505 domain_name=domain.name, 

5506 channel_name=channel.name, 

5507 signal_name=signal.name, 

5508 ) 

5509 

5510 if not ordinates: 

5511 raise ValueError("No time-response signals found in this dataset.") 

5512 

5513 ordinate = np.concatenate(ordinates) 

5514 abscissa = np.concatenate(abscissas) 

5515 coordinate = np.concatenate(response_coords)[..., np.newaxis] 

5516 return time_history_array( 

5517 abscissa, 

5518 ordinate, 

5519 coordinate, 

5520 comment1s, 

5521 comment2s, 

5522 comment3s, 

5523 comment4s, 

5524 comment5s, 

5525 ) 

5526 

5527 def get_geometry(self) -> Geometry: 

5528 """Build an ``sdynpy`` geometry object from measurement points and elements. 

5529 

5530 Measurement-point coordinates are converted into nodes, and simple 

5531 element connectivity is inferred from element point-label counts: 

5532 2-point elements become tracelines, 3-point elements become type 61 

5533 elements, and 4-point elements become type 64 elements. 

5534 """ 

5535 coordinate_system = coordinate_system_array(1) 

5536 coords = self.measurement_points.coordinates_to_array 

5537 node_ids = [pt.label for pt in self.measurement_points] 

5538 nodes = node_array(node_ids, coords) 

5539 geometry = Geometry(nodes, coordinate_system) 

5540 

5541 for element in self.elements: 

5542 labels = element.meas_point_labels 

5543 if len(labels) == 2: 

5544 geometry.add_traceline(labels) 

5545 elif len(labels) == 3: 

5546 geometry.add_element(61, labels) 

5547 elif len(labels) == 4: 

5548 geometry.add_element(64, labels) 

5549 

5550 return geometry 

5551 

5552 def _get_contained_files_or_none(self) -> list["PolyFile"] | None: 

5553 """Return contained files if this is a combined file, else ``None``.""" 

5554 try: 

5555 contained = self.contained_files 

5556 except ValueError: 

5557 return None 

5558 return contained if contained else None 

5559 

5560 def _delegate_image_method_to_contained_files( 

5561 self, 

5562 method_name: str, 

5563 /, 

5564 *args: Any, 

5565 **kwargs: Any, 

5566 ) -> list[Any]: 

5567 """Call an image-producing method recursively on contained files.""" 

5568 contained = self._get_contained_files_or_none() 

5569 if contained is None: 

5570 raise ValueError( 

5571 "_delegate_image_method_to_contained_files called on a file " 

5572 "without contained files." 

5573 ) 

5574 

5575 kwargs = dict(kwargs) 

5576 if "ax" in kwargs: 

5577 kwargs["ax"] = None 

5578 

5579 out: list[Any] = [] 

5580 for child in contained: 

5581 method = getattr(child, method_name) 

5582 out.append(method(*args, **kwargs)) 

5583 return out 

5584 

5585 def get_image(self) -> Image.Image | list[Any]: 

5586 """Return the stored test image as a PIL image. 

5587 

5588 For combined files, returns a list of images from the contained files. 

5589 """ 

5590 contained = self._get_contained_files_or_none() 

5591 if contained is not None: 

5592 return self._delegate_image_method_to_contained_files("get_image") 

5593 

5594 return self.video_bitmap.get_image().copy() 

5595 

5596 @staticmethod 

5597 def _pil_color(color: str) -> str: 

5598 """Convert Matplotlib-style short color codes to PIL-compatible names.""" 

5599 color_map = { 

5600 "r": "red", 

5601 "g": "green", 

5602 "b": "blue", 

5603 "c": "cyan", 

5604 "m": "magenta", 

5605 "y": "yellow", 

5606 "k": "black", 

5607 "w": "white", 

5608 } 

5609 return color_map.get(color, color) 

5610 

5611 @staticmethod 

5612 def _video_to_pixel( 

5613 x: float, 

5614 y: float, 

5615 video_rect: tuple[float, float, float, float], 

5616 image_width: int, 

5617 image_height: int, 

5618 ) -> tuple[float, float]: 

5619 """Convert Polytec video coordinates to image pixel coordinates.""" 

5620 left, top, right, bottom = video_rect 

5621 x_pixel = (x - left) * image_width / (right - left) 

5622 y_pixel = (y - top) * image_height / (bottom - top) 

5623 return x_pixel, y_pixel 

5624 

5625 @staticmethod 

5626 def _default_alignment_colors() -> list[str]: 

5627 """Default color cycle for plotting multiple alignments.""" 

5628 return ["red", "cyan", "yellow", "magenta", "lime", "orange", "white"] 

5629 

5630 @staticmethod 

5631 def _default_alignment_markers() -> list[str]: 

5632 """Default marker cycle for plotting multiple alignments.""" 

5633 return ["o", "x", "^", "s", "d", "+", "*"] 

5634 

5635 @staticmethod 

5636 def _default_status_colors() -> list[str]: 

5637 """Default color cycle for status plotting.""" 

5638 return [ 

5639 "red", 

5640 "cyan", 

5641 "yellow", 

5642 "magenta", 

5643 "lime", 

5644 "orange", 

5645 "white", 

5646 "deepskyblue", 

5647 "springgreen", 

5648 "gold", 

5649 ] 

5650 

5651 @staticmethod 

5652 def _default_status_markers() -> list[str]: 

5653 """Default marker cycle for status plotting.""" 

5654 return ["o", "x", "^", "s", "d", "+", "*", "v", "<", ">"] 

5655 

5656 @staticmethod 

5657 def _decode_flag_label(value: int, enum_cls: type[IntFlag]) -> str: 

5658 """Convert a raw IntFlag value into a readable label.""" 

5659 try: 

5660 flags = enum_cls(int(value)) 

5661 except Exception: 

5662 return str(value) 

5663 

5664 if int(value) == 0: 

5665 zero_member = None 

5666 for member in enum_cls: 

5667 if int(member.value) == 0: 

5668 zero_member = member.name 

5669 break 

5670 return zero_member or "NONE" 

5671 

5672 names: list[str] = [] 

5673 for member in enum_cls: 

5674 if int(member.value) == 0: 

5675 continue 

5676 if flags & member: 

5677 names.append(member.name) 

5678 

5679 return " | ".join(names) if names else str(int(value)) 

5680 

5681 def _plot_points_on_image( 

5682 self, 

5683 points_xy: list[tuple[float, float]], 

5684 *, 

5685 image: Image.Image | None = None, 

5686 video_rect: tuple[float, float, float, float] | None = None, 

5687 mode: str = "matplotlib", 

5688 color: str = "red", 

5689 marker: str = "o", 

5690 size: float = 40.0, 

5691 linewidth: float = 2.0, 

5692 show_labels: bool = False, 

5693 labels: list[str] | None = None, 

5694 label_color: str | None = None, 

5695 ax: Any = None, 

5696 title: str | None = None, 

5697 ) -> Any: 

5698 """Plot video-coordinate points on the stored image.""" 

5699 if image is None: 

5700 image = self.get_image() 

5701 if isinstance(image, list): 

5702 raise ValueError( 

5703 "Combined files return multiple images. This method should " 

5704 "only be called on a leaf file." 

5705 ) 

5706 else: 

5707 image = image.copy() 

5708 

5709 if video_rect is None: 

5710 raise ValueError("`video_rect` must be provided.") 

5711 

5712 image_width, image_height = image.size 

5713 pixel_points = [ 

5714 self._video_to_pixel(x, y, video_rect, image_width, image_height) for x, y in points_xy 

5715 ] 

5716 

5717 if label_color is None: 

5718 label_color = color 

5719 

5720 if mode.lower() == "pil": 

5721 draw = ImageDraw.Draw(image) 

5722 radius = max(2.0, float(linewidth) + 2.0) 

5723 pil_color = self._pil_color(color) 

5724 pil_label_color = self._pil_color(label_color) 

5725 

5726 for i, (xp, yp) in enumerate(pixel_points): 

5727 draw.ellipse( 

5728 (xp - radius, yp - radius, xp + radius, yp + radius), 

5729 outline=pil_color, 

5730 width=max(1, int(round(linewidth))), 

5731 ) 

5732 if show_labels and labels is not None and i < len(labels): 

5733 draw.text( 

5734 (xp + radius + 2, yp + radius + 2), 

5735 str(labels[i]), 

5736 fill=pil_label_color, 

5737 ) 

5738 return image 

5739 

5740 if mode.lower() == "matplotlib": 

5741 

5742 created_fig = False 

5743 if ax is None: 

5744 fig, ax = plt.subplots() 

5745 created_fig = True 

5746 else: 

5747 fig = ax.figure 

5748 

5749 if not ax.images: 

5750 ax.imshow(image) 

5751 

5752 xs = [p[0] for p in pixel_points] 

5753 ys = [p[1] for p in pixel_points] 

5754 ax.scatter(xs, ys, c=color, marker=marker, s=size) 

5755 

5756 if show_labels and labels is not None: 

5757 for (xp, yp), label in zip(pixel_points, labels): 

5758 ax.text(xp, yp, str(label), color=label_color) 

5759 

5760 ax.set_xlim(0, image_width) 

5761 ax.set_ylim(image_height, 0) 

5762 ax.set_aspect("equal") 

5763 ax.set_xlabel("Pixel X") 

5764 ax.set_ylabel("Pixel Y") 

5765 

5766 if title is not None: 

5767 ax.set_title(title) 

5768 

5769 if created_fig: 

5770 fig.tight_layout() 

5771 

5772 return fig, ax 

5773 

5774 raise ValueError("`mode` must be either 'pil' or 'matplotlib'.") 

5775 

5776 def get_measpoint_image( 

5777 self, 

5778 *, 

5779 mode: str = "matplotlib", 

5780 color: str = "red", 

5781 marker: str = "o", 

5782 size: float = 40.0, 

5783 linewidth: float = 2.0, 

5784 show_labels: bool = False, 

5785 use_point_labels: bool = True, 

5786 ax: Any = None, 

5787 title: str | None = "Measurement Points", 

5788 ) -> Any: 

5789 """Plot measurement-point locations on the stored image. 

5790 

5791 For combined files, returns a list of per-contained-file results. 

5792 """ 

5793 contained = self._get_contained_files_or_none() 

5794 if contained is not None: 

5795 return self._delegate_image_method_to_contained_files( 

5796 "get_measpoint_image", 

5797 mode=mode, 

5798 color=color, 

5799 marker=marker, 

5800 size=size, 

5801 linewidth=linewidth, 

5802 show_labels=show_labels, 

5803 use_point_labels=use_point_labels, 

5804 ax=ax, 

5805 title=title, 

5806 ) 

5807 

5808 image = self.get_image() 

5809 video_rect = tuple(float(v) for v in self.measurement_points.get_video_rect()) 

5810 

5811 points_xy: list[tuple[float, float]] = [] 

5812 labels: list[str] = [] 

5813 

5814 for pt in self.measurement_points: 

5815 try: 

5816 x, y = pt.video_xy() 

5817 except Exception: 

5818 continue 

5819 points_xy.append((float(x), float(y))) 

5820 labels.append(str(pt.label if use_point_labels else pt.index)) 

5821 

5822 return self._plot_points_on_image( 

5823 points_xy, 

5824 image=image, 

5825 video_rect=video_rect, 

5826 mode=mode, 

5827 color=color, 

5828 marker=marker, 

5829 size=size, 

5830 linewidth=linewidth, 

5831 show_labels=show_labels, 

5832 labels=labels, 

5833 ax=ax, 

5834 title=title, 

5835 ) 

5836 

5837 def get_alignment2d_image( 

5838 self, 

5839 alignment_index: int | None = None, 

5840 *, 

5841 mode: str = "matplotlib", 

5842 color: str | None = None, 

5843 marker: str | None = None, 

5844 size: float = 60.0, 

5845 linewidth: float = 2.0, 

5846 show_labels: bool = True, 

5847 ax: Any = None, 

5848 title: str | None = "2D Alignment Points", 

5849 ) -> Any: 

5850 """Plot 2D alignment points on the stored image. 

5851 

5852 For combined files, returns a list of per-contained-file results. 

5853 """ 

5854 contained = self._get_contained_files_or_none() 

5855 if contained is not None: 

5856 return self._delegate_image_method_to_contained_files( 

5857 "get_alignment2d_image", 

5858 alignment_index, 

5859 mode=mode, 

5860 color=color, 

5861 marker=marker, 

5862 size=size, 

5863 linewidth=linewidth, 

5864 show_labels=show_labels, 

5865 ax=ax, 

5866 title=title, 

5867 ) 

5868 

5869 alignments = self.alignments.alignments_2d 

5870 if alignment_index is None: 

5871 selected_alignments = list(enumerate(alignments)) 

5872 else: 

5873 selected_alignments = [(alignment_index, alignments[alignment_index])] 

5874 

5875 image = self.get_image() 

5876 video_rect = tuple(float(v) for v in self.alignments.get_video_rect()) 

5877 

5878 default_colors = self._default_alignment_colors() 

5879 default_markers = self._default_alignment_markers() 

5880 

5881 if mode.lower() == "pil": 

5882 out_image = image.copy() 

5883 for i_align, alignment in selected_alignments: 

5884 this_color = ( 

5885 color if color is not None else default_colors[i_align % len(default_colors)] 

5886 ) 

5887 this_marker = ( 

5888 marker 

5889 if marker is not None 

5890 else default_markers[i_align % len(default_markers)] 

5891 ) 

5892 

5893 points_xy = [ 

5894 (float(pt.video_x), float(pt.video_y)) for pt in alignment.align_2d_points 

5895 ] 

5896 labels = [f"A{i_align}:{i}" for i in range(len(points_xy))] 

5897 

5898 out_image = self._plot_points_on_image( 

5899 points_xy, 

5900 image=out_image, 

5901 video_rect=video_rect, 

5902 mode="pil", 

5903 color=this_color, 

5904 marker=this_marker, 

5905 size=size, 

5906 linewidth=linewidth, 

5907 show_labels=show_labels, 

5908 labels=labels, 

5909 title=None, 

5910 ) 

5911 return out_image 

5912 

5913 if mode.lower() == "matplotlib": 

5914 created_fig = False 

5915 if ax is None: 

5916 fig, ax = plt.subplots() 

5917 created_fig = True 

5918 ax.imshow(image) 

5919 else: 

5920 fig = ax.figure 

5921 if not ax.images: 

5922 ax.imshow(image) 

5923 

5924 image_width, image_height = image.size 

5925 

5926 for i_align, alignment in selected_alignments: 

5927 this_color = ( 

5928 color if color is not None else default_colors[i_align % len(default_colors)] 

5929 ) 

5930 this_marker = ( 

5931 marker 

5932 if marker is not None 

5933 else default_markers[i_align % len(default_markers)] 

5934 ) 

5935 

5936 points_xy = [ 

5937 (float(pt.video_x), float(pt.video_y)) for pt in alignment.align_2d_points 

5938 ] 

5939 pixel_points = [ 

5940 self._video_to_pixel(x, y, video_rect, image_width, image_height) 

5941 for x, y in points_xy 

5942 ] 

5943 xs = [p[0] for p in pixel_points] 

5944 ys = [p[1] for p in pixel_points] 

5945 

5946 label = f"2D Alignment {i_align}" if alignment_index is None else "2D Alignment" 

5947 ax.scatter(xs, ys, c=this_color, marker=this_marker, s=size, label=label) 

5948 

5949 if show_labels: 

5950 for j, (xp, yp) in enumerate(pixel_points): 

5951 ax.text(xp, yp, f"A{i_align}:{j}", color=this_color) 

5952 

5953 ax.set_xlim(0, image_width) 

5954 ax.set_ylim(image_height, 0) 

5955 ax.set_aspect("equal") 

5956 ax.set_xlabel("Pixel X") 

5957 ax.set_ylabel("Pixel Y") 

5958 

5959 if title is not None: 

5960 ax.set_title(title) 

5961 

5962 if alignment_index is None and len(selected_alignments) > 1: 

5963 ax.legend() 

5964 

5965 if created_fig: 

5966 fig.tight_layout() 

5967 

5968 return fig, ax 

5969 

5970 raise ValueError("`mode` must be either 'pil' or 'matplotlib'.") 

5971 

5972 def get_alignment3d_image( 

5973 self, 

5974 alignment_index: int | None = None, 

5975 *, 

5976 mode: str = "matplotlib", 

5977 color: str | None = None, 

5978 marker: str | None = None, 

5979 size: float = 60.0, 

5980 linewidth: float = 2.0, 

5981 show_labels: bool = True, 

5982 use_point_labels: bool = True, 

5983 ax: Any = None, 

5984 title: str | None = "3D Alignment Points", 

5985 ) -> Any: 

5986 """Plot 3D alignment points on the stored image. 

5987 

5988 For combined files, returns a list of per-contained-file results. 

5989 """ 

5990 contained = self._get_contained_files_or_none() 

5991 if contained is not None: 

5992 return self._delegate_image_method_to_contained_files( 

5993 "get_alignment3d_image", 

5994 alignment_index, 

5995 mode=mode, 

5996 color=color, 

5997 marker=marker, 

5998 size=size, 

5999 linewidth=linewidth, 

6000 show_labels=show_labels, 

6001 use_point_labels=use_point_labels, 

6002 ax=ax, 

6003 title=title, 

6004 ) 

6005 

6006 alignments = self.alignments.alignments_3d 

6007 if alignment_index is None: 

6008 selected_alignments = list(enumerate(alignments)) 

6009 else: 

6010 selected_alignments = [(alignment_index, alignments[alignment_index])] 

6011 

6012 image = self.get_image() 

6013 video_rect = tuple(float(v) for v in self.alignments.get_video_rect()) 

6014 

6015 default_colors = self._default_alignment_colors() 

6016 default_markers = self._default_alignment_markers() 

6017 

6018 if mode.lower() == "pil": 

6019 out_image = image.copy() 

6020 for i_align, alignment in selected_alignments: 

6021 this_color = ( 

6022 color if color is not None else default_colors[i_align % len(default_colors)] 

6023 ) 

6024 this_marker = ( 

6025 marker 

6026 if marker is not None 

6027 else default_markers[i_align % len(default_markers)] 

6028 ) 

6029 

6030 points_xy = [ 

6031 (float(pt.video_x), float(pt.video_y)) for pt in alignment.align_3d_points 

6032 ] 

6033 if use_point_labels: 

6034 labels = [str(pt.label) for pt in alignment.align_3d_points] 

6035 else: 

6036 labels = [f"A{i_align}:{i}" for i in range(len(points_xy))] 

6037 

6038 out_image = self._plot_points_on_image( 

6039 points_xy, 

6040 image=out_image, 

6041 video_rect=video_rect, 

6042 mode="pil", 

6043 color=this_color, 

6044 marker=this_marker, 

6045 size=size, 

6046 linewidth=linewidth, 

6047 show_labels=show_labels, 

6048 labels=labels, 

6049 title=None, 

6050 ) 

6051 return out_image 

6052 

6053 if mode.lower() == "matplotlib": 

6054 created_fig = False 

6055 if ax is None: 

6056 fig, ax = plt.subplots() 

6057 created_fig = True 

6058 ax.imshow(image) 

6059 else: 

6060 fig = ax.figure 

6061 if not ax.images: 

6062 ax.imshow(image) 

6063 

6064 image_width, image_height = image.size 

6065 

6066 for i_align, alignment in selected_alignments: 

6067 this_color = ( 

6068 color if color is not None else default_colors[i_align % len(default_colors)] 

6069 ) 

6070 this_marker = ( 

6071 marker 

6072 if marker is not None 

6073 else default_markers[i_align % len(default_markers)] 

6074 ) 

6075 

6076 points_xy = [ 

6077 (float(pt.video_x), float(pt.video_y)) for pt in alignment.align_3d_points 

6078 ] 

6079 pixel_points = [ 

6080 self._video_to_pixel(x, y, video_rect, image_width, image_height) 

6081 for x, y in points_xy 

6082 ] 

6083 xs = [p[0] for p in pixel_points] 

6084 ys = [p[1] for p in pixel_points] 

6085 

6086 label = f"3D Alignment {i_align}" if alignment_index is None else "3D Alignment" 

6087 ax.scatter(xs, ys, c=this_color, marker=this_marker, s=size, label=label) 

6088 

6089 if show_labels: 

6090 if use_point_labels: 

6091 point_labels = [str(pt.label) for pt in alignment.align_3d_points] 

6092 else: 

6093 point_labels = [f"A{i_align}:{j}" for j in range(len(pixel_points))] 

6094 for (xp, yp), point_label in zip(pixel_points, point_labels): 

6095 ax.text(xp, yp, point_label, color=this_color) 

6096 

6097 ax.set_xlim(0, image_width) 

6098 ax.set_ylim(image_height, 0) 

6099 ax.set_aspect("equal") 

6100 ax.set_xlabel("Pixel X") 

6101 ax.set_ylabel("Pixel Y") 

6102 

6103 if title is not None: 

6104 ax.set_title(title) 

6105 

6106 if alignment_index is None and len(selected_alignments) > 1: 

6107 ax.legend() 

6108 

6109 if created_fig: 

6110 fig.tight_layout() 

6111 

6112 return fig, ax 

6113 

6114 raise ValueError("`mode` must be either 'pil' or 'matplotlib'.") 

6115 

6116 def _get_status_image( 

6117 self, 

6118 *, 

6119 status_attr: str, 

6120 enum_cls: type[IntFlag], 

6121 mode: str = "matplotlib", 

6122 size: float = 60.0, 

6123 linewidth: float = 2.0, 

6124 show_labels: bool = False, 

6125 use_point_labels: bool = True, 

6126 ax: Any = None, 

6127 title: str | None = None, 

6128 ) -> Any: 

6129 """Plot measurement points grouped by status flags.""" 

6130 image = self.get_image() 

6131 if isinstance(image, list): 

6132 raise ValueError( 

6133 "Combined files should be handled by the public status-image " 

6134 "methods before calling _get_status_image." 

6135 ) 

6136 

6137 video_rect = tuple(float(v) for v in self.measurement_points.get_video_rect()) 

6138 

6139 grouped_points: dict[int, list[tuple[float, float]]] = defaultdict(list) 

6140 grouped_labels: dict[int, list[str]] = defaultdict(list) 

6141 

6142 for pt in self.measurement_points: 

6143 try: 

6144 x, y = pt.video_xy() 

6145 except Exception: 

6146 continue 

6147 

6148 status_value = int(getattr(pt, status_attr)) 

6149 grouped_points[status_value].append((float(x), float(y))) 

6150 grouped_labels[status_value].append(str(pt.label if use_point_labels else pt.index)) 

6151 

6152 if not grouped_points: 

6153 raise ValueError(f"No points with plottable {status_attr} data were found.") 

6154 

6155 unique_status_values = sorted(grouped_points.keys()) 

6156 

6157 colors = self._default_status_colors() 

6158 markers = self._default_status_markers() 

6159 

6160 if mode.lower() == "pil": 

6161 out_image = image.copy() 

6162 for i_status, status_value in enumerate(unique_status_values): 

6163 this_color = colors[i_status % len(colors)] 

6164 this_marker = markers[i_status % len(markers)] 

6165 labels = grouped_labels[status_value] if show_labels else None 

6166 

6167 out_image = self._plot_points_on_image( 

6168 grouped_points[status_value], 

6169 image=out_image, 

6170 video_rect=video_rect, 

6171 mode="pil", 

6172 color=this_color, 

6173 marker=this_marker, 

6174 size=size, 

6175 linewidth=linewidth, 

6176 show_labels=show_labels, 

6177 labels=labels, 

6178 title=None, 

6179 ) 

6180 return out_image 

6181 

6182 if mode.lower() == "matplotlib": 

6183 created_fig = False 

6184 if ax is None: 

6185 fig, ax = plt.subplots() 

6186 created_fig = True 

6187 ax.imshow(image) 

6188 else: 

6189 fig = ax.figure 

6190 if not ax.images: 

6191 ax.imshow(image) 

6192 

6193 image_width, image_height = image.size 

6194 

6195 for i_status, status_value in enumerate(unique_status_values): 

6196 this_color = colors[i_status % len(colors)] 

6197 this_marker = markers[i_status % len(markers)] 

6198 

6199 pixel_points = [ 

6200 self._video_to_pixel(x, y, video_rect, image_width, image_height) 

6201 for x, y in grouped_points[status_value] 

6202 ] 

6203 xs = [p[0] for p in pixel_points] 

6204 ys = [p[1] for p in pixel_points] 

6205 

6206 legend_label = self._decode_flag_label(status_value, enum_cls) 

6207 

6208 ax.scatter( 

6209 xs, 

6210 ys, 

6211 c=this_color, 

6212 marker=this_marker, 

6213 s=size, 

6214 label=legend_label, 

6215 ) 

6216 

6217 if show_labels: 

6218 point_labels = grouped_labels[status_value] 

6219 for (xp, yp), point_label in zip(pixel_points, point_labels): 

6220 ax.text(xp, yp, point_label, color=this_color) 

6221 

6222 ax.set_xlim(0, image_width) 

6223 ax.set_ylim(image_height, 0) 

6224 ax.set_aspect("equal") 

6225 ax.set_xlabel("Pixel X") 

6226 ax.set_ylabel("Pixel Y") 

6227 

6228 if title is not None: 

6229 ax.set_title(title) 

6230 

6231 ax.legend() 

6232 

6233 if created_fig: 

6234 fig.tight_layout() 

6235 

6236 return fig, ax 

6237 

6238 raise ValueError("`mode` must be either 'pil' or 'matplotlib'.") 

6239 

6240 def get_geometry_status_image( 

6241 self, 

6242 *, 

6243 mode: str = "matplotlib", 

6244 size: float = 60.0, 

6245 linewidth: float = 2.0, 

6246 show_labels: bool = False, 

6247 use_point_labels: bool = True, 

6248 ax: Any = None, 

6249 title: str | None = "Geometry Status", 

6250 ) -> Any: 

6251 """Plot measurement points grouped by geometry status. 

6252 

6253 For combined files, returns a list of per-contained-file results. 

6254 """ 

6255 contained = self._get_contained_files_or_none() 

6256 if contained is not None: 

6257 return self._delegate_image_method_to_contained_files( 

6258 "get_geometry_status_image", 

6259 mode=mode, 

6260 size=size, 

6261 linewidth=linewidth, 

6262 show_labels=show_labels, 

6263 use_point_labels=use_point_labels, 

6264 ax=ax, 

6265 title=title, 

6266 ) 

6267 

6268 return self._get_status_image( 

6269 status_attr="geometry_status", 

6270 enum_cls=GeometryStatus, 

6271 mode=mode, 

6272 size=size, 

6273 linewidth=linewidth, 

6274 show_labels=show_labels, 

6275 use_point_labels=use_point_labels, 

6276 ax=ax, 

6277 title=title, 

6278 ) 

6279 

6280 def get_focus_status_image( 

6281 self, 

6282 *, 

6283 mode: str = "matplotlib", 

6284 size: float = 60.0, 

6285 linewidth: float = 2.0, 

6286 show_labels: bool = False, 

6287 use_point_labels: bool = True, 

6288 ax: Any = None, 

6289 title: str | None = "Focus Status", 

6290 ) -> Any: 

6291 """Plot measurement points grouped by focus status. 

6292 

6293 For combined files, returns a list of per-contained-file results. 

6294 """ 

6295 contained = self._get_contained_files_or_none() 

6296 if contained is not None: 

6297 return self._delegate_image_method_to_contained_files( 

6298 "get_focus_status_image", 

6299 mode=mode, 

6300 size=size, 

6301 linewidth=linewidth, 

6302 show_labels=show_labels, 

6303 use_point_labels=use_point_labels, 

6304 ax=ax, 

6305 title=title, 

6306 ) 

6307 

6308 return self._get_status_image( 

6309 status_attr="focus_status", 

6310 enum_cls=FocusStatus, 

6311 mode=mode, 

6312 size=size, 

6313 linewidth=linewidth, 

6314 show_labels=show_labels, 

6315 use_point_labels=use_point_labels, 

6316 ax=ax, 

6317 title=title, 

6318 ) 

6319 

6320 def get_scan_status_image( 

6321 self, 

6322 *, 

6323 mode: str = "matplotlib", 

6324 size: float = 60.0, 

6325 linewidth: float = 2.0, 

6326 show_labels: bool = False, 

6327 use_point_labels: bool = True, 

6328 ax: Any = None, 

6329 title: str | None = "Scan Status", 

6330 ) -> Any: 

6331 """Plot measurement points grouped by scan status. 

6332 

6333 For combined files, returns a list of per-contained-file results. 

6334 """ 

6335 contained = self._get_contained_files_or_none() 

6336 if contained is not None: 

6337 return self._delegate_image_method_to_contained_files( 

6338 "get_scan_status_image", 

6339 mode=mode, 

6340 size=size, 

6341 linewidth=linewidth, 

6342 show_labels=show_labels, 

6343 use_point_labels=use_point_labels, 

6344 ax=ax, 

6345 title=title, 

6346 ) 

6347 

6348 return self._get_status_image( 

6349 status_attr="scan_status", 

6350 enum_cls=ScanStatus, 

6351 mode=mode, 

6352 size=size, 

6353 linewidth=linewidth, 

6354 show_labels=show_labels, 

6355 use_point_labels=use_point_labels, 

6356 ax=ax, 

6357 title=title, 

6358 ) 

6359 

6360 def get_alignment3d_quality(self): 

6361 """Returns the alignment quality for each laser head. 

6362 

6363 If the file contains multiple files, then one quality score 

6364 will be returned for each contained file. 

6365 

6366 Returns 

6367 ------- 

6368 quality : np.ndarray 

6369 A 1D array for single-scan files and a 2D array for combined files. 

6370 Each row in the 2D array represents a different scan. 

6371 """ 

6372 contained = self._get_contained_files_or_none() 

6373 if contained: 

6374 quality = [] 

6375 for file in self.contained_files: 

6376 quality.append( 

6377 [alignment.current_quality for alignment in file.alignments.alignments_3d] 

6378 ) 

6379 else: 

6380 quality = [alignment.current_quality for alignment in self.alignments.alignments_3d] 

6381 return np.array(quality) 

6382 

6383 @staticmethod 

6384 def _pixel_to_video( 

6385 x_pixel: float, 

6386 y_pixel: float, 

6387 video_rect: tuple[float, float, float, float], 

6388 image_width: int, 

6389 image_height: int, 

6390 ) -> tuple[float, float]: 

6391 """Convert image pixel coordinates to Polytec video coordinates.""" 

6392 left, top, right, bottom = video_rect 

6393 x_video = left + x_pixel * (right - left) / image_width 

6394 y_video = top + y_pixel * (bottom - top) / image_height 

6395 return x_video, y_video 

6396 

6397 def pixel_to_video( 

6398 self, 

6399 x_pixel: float, 

6400 y_pixel: float, 

6401 ) -> tuple[float, float] | list[Any]: 

6402 """Convert image pixel coordinates to Polytec video coordinates. 

6403 

6404 For combined files, returns a list of per-contained-file results. 

6405 """ 

6406 contained = self._get_contained_files_or_none() 

6407 if contained is not None: 

6408 return self._delegate_image_method_to_contained_files( 

6409 "pixel_to_video", 

6410 x_pixel, 

6411 y_pixel, 

6412 ) 

6413 

6414 image = self.get_image() 

6415 if isinstance(image, list): 

6416 raise ValueError( 

6417 "Combined files should be handled before calling pixel_to_video." 

6418 ) 

6419 

6420 video_rect = tuple(float(v) for v in self.alignments.get_video_rect()) 

6421 image_width, image_height = image.size 

6422 return self._pixel_to_video( 

6423 x_pixel, y_pixel, video_rect, image_width, image_height 

6424 ) 

6425 

6426 def get_scanner_coordinates_at_pixel( 

6427 self, 

6428 x_pixel: float, 

6429 y_pixel: float, 

6430 ) -> list[dict[str, Any]] | list[Any]: 

6431 """Return scanner coordinates for each 2D alignment at an image pixel. 

6432 

6433 Parameters 

6434 ---------- 

6435 x_pixel, y_pixel 

6436 Image pixel coordinates. 

6437 

6438 Returns 

6439 ------- 

6440 list[dict[str, Any]] | list[Any] 

6441 For a normal file, returns a list of dictionaries, one per 2D 

6442 alignment, containing: 

6443 - ``alignment_index`` 

6444 - ``video_xy`` 

6445 - ``scanner_xy`` 

6446 

6447 For a combined file, returns a list of per-contained-file results. 

6448 """ 

6449 contained = self._get_contained_files_or_none() 

6450 if contained is not None: 

6451 return self._delegate_image_method_to_contained_files( 

6452 "get_scanner_coordinates_at_pixel", 

6453 x_pixel, 

6454 y_pixel, 

6455 ) 

6456 

6457 image = self.get_image() 

6458 if isinstance(image, list): 

6459 raise ValueError( 

6460 "Combined files should be handled before calling " 

6461 "get_scanner_coordinates_at_pixel." 

6462 ) 

6463 

6464 video_rect = tuple(float(v) for v in self.alignments.get_video_rect()) 

6465 image_width, image_height = image.size 

6466 video_x, video_y = self._pixel_to_video( 

6467 x_pixel, y_pixel, video_rect, image_width, image_height 

6468 ) 

6469 

6470 results: list[dict[str, Any]] = [] 

6471 for i, alignment2d in enumerate(self.alignments.alignments_2d): 

6472 scanner_xy = alignment2d.video_to_scanner(video_x, video_y) 

6473 try: 

6474 scanner_x, scanner_y = scanner_xy 

6475 except Exception as e: 

6476 raise ValueError( 

6477 f"Alignment2D.video_to_scanner did not return a 2-tuple for " 

6478 f"alignment index {i}: {scanner_xy!r}" 

6479 ) from e 

6480 

6481 results.append( 

6482 { 

6483 "alignment_index": i, 

6484 "video_xy": (float(video_x), float(video_y)), 

6485 "scanner_xy": (float(scanner_x), float(scanner_y)), 

6486 } 

6487 ) 

6488 

6489 return results 

6490 

6491 def get_beam_rays_at_pixel( 

6492 self, 

6493 x_pixel: float, 

6494 y_pixel: float, 

6495 *, 

6496 distance0: float = 0.0, 

6497 distance1: float = 1.0, 

6498 ) -> list[dict[str, Any]] | list[Any]: 

6499 """Return beam origin and unit direction for each scan head at a pixel. 

6500 

6501 Parameters 

6502 ---------- 

6503 x_pixel, y_pixel 

6504 Image pixel coordinates. 

6505 distance0, distance1 

6506 Distances passed to ``Alignment3D.scanner_to_coord_3d`` to construct 

6507 two points along the beam. ``distance0`` defines the origin point, 

6508 and the vector from that point to the point at ``distance1`` is 

6509 normalized to produce the unit beam direction. 

6510 

6511 Returns 

6512 ------- 

6513 list[dict[str, Any]] | list[Any] 

6514 For a normal file, returns a list of dictionaries, one per matched 

6515 2D/3D alignment pair, containing: 

6516 - ``alignment_index`` 

6517 - ``video_xy`` 

6518 - ``scanner_xy`` 

6519 - ``origin`` 

6520 - ``direction`` 

6521 

6522 For a combined file, returns a list of per-contained-file results. 

6523 

6524 Raises 

6525 ------ 

6526 ValueError 

6527 If the number of 2D and 3D alignments does not match, or if a zero 

6528 direction vector is encountered. 

6529 """ 

6530 contained = self._get_contained_files_or_none() 

6531 if contained is not None: 

6532 return self._delegate_image_method_to_contained_files( 

6533 "get_beam_rays_at_pixel", 

6534 x_pixel, 

6535 y_pixel, 

6536 distance0=distance0, 

6537 distance1=distance1, 

6538 ) 

6539 

6540 alignments_2d = self.alignments.alignments_2d 

6541 alignments_3d = self.alignments.alignments_3d 

6542 

6543 if len(alignments_2d) != len(alignments_3d): 

6544 raise ValueError( 

6545 "The number of 2D alignments does not match the number of 3D " 

6546 "alignments, so scan-head pairing by index is ambiguous." 

6547 ) 

6548 

6549 scanner_results = self.get_scanner_coordinates_at_pixel(x_pixel, y_pixel) 

6550 if not isinstance(scanner_results, list): 

6551 raise ValueError( 

6552 "Unexpected result from get_scanner_coordinates_at_pixel." 

6553 ) 

6554 

6555 results: list[dict[str, Any]] = [] 

6556 

6557 for result in scanner_results: 

6558 i = int(result["alignment_index"]) 

6559 scanner_x, scanner_y = result["scanner_xy"] 

6560 alignment3d = alignments_3d[i] 

6561 

6562 p0 = alignment3d.scanner_to_coord_3d(scanner_x, scanner_y, distance0) 

6563 p1 = alignment3d.scanner_to_coord_3d(scanner_x, scanner_y, distance1) 

6564 

6565 try: 

6566 p0_arr = np.asarray(p0, dtype=float).reshape(3) 

6567 p1_arr = np.asarray(p1, dtype=float).reshape(3) 

6568 except Exception as exc: 

6569 raise ValueError( 

6570 f"Alignment3D.scanner_to_coord_3d returned unexpected values " 

6571 f"for alignment index {i}: p0={p0!r}, p1={p1!r}" 

6572 ) from exc 

6573 

6574 direction = p1_arr - p0_arr 

6575 norm = np.linalg.norm(direction) 

6576 if norm == 0.0: 

6577 raise ValueError( 

6578 f"Zero-length beam direction encountered for alignment index {i}." 

6579 ) 

6580 direction_unit = direction / norm 

6581 

6582 results.append( 

6583 { 

6584 "alignment_index": i, 

6585 "video_xy": result["video_xy"], 

6586 "scanner_xy": (float(scanner_x), float(scanner_y)), 

6587 "origin": p0_arr, 

6588 "direction": direction_unit, 

6589 } 

6590 ) 

6591 

6592 return results 

6593 

6594 def get_beam_intersection_at_pixel( 

6595 self, 

6596 x_pixel: float, 

6597 y_pixel: float, 

6598 *, 

6599 distance0: float = 0.0, 

6600 distance1: float = 1.0, 

6601 ) -> dict[str, Any] | list[Any]: 

6602 """Compute the least-squares beam intersection point at an image pixel. 

6603 

6604 Parameters 

6605 ---------- 

6606 x_pixel, y_pixel 

6607 Image pixel coordinates. 

6608 distance0, distance1 

6609 Distances passed to ``Alignment3D.scanner_to_coord_3d`` to construct 

6610 two points on each beam. 

6611 

6612 Returns 

6613 ------- 

6614 dict[str, Any] | list[Any] 

6615 For a normal file, returns a dictionary containing: 

6616 - ``pixel_xy`` 

6617 - ``video_xy`` 

6618 - ``intersection`` 

6619 - ``distances`` 

6620 - ``rms_distance`` 

6621 - ``mean_distance`` 

6622 - ``max_distance`` 

6623 - ``beam_rays`` 

6624 

6625 For a combined file, returns a list of per-contained-file results. 

6626 

6627 Raises 

6628 ------ 

6629 ValueError 

6630 If fewer than two beam rays are available. 

6631 """ 

6632 contained = self._get_contained_files_or_none() 

6633 if contained is not None: 

6634 return self._delegate_image_method_to_contained_files( 

6635 "get_beam_intersection_at_pixel", 

6636 x_pixel, 

6637 y_pixel, 

6638 distance0=distance0, 

6639 distance1=distance1, 

6640 ) 

6641 

6642 beam_rays = self.get_beam_rays_at_pixel( 

6643 x_pixel, 

6644 y_pixel, 

6645 distance0=distance0, 

6646 distance1=distance1, 

6647 ) 

6648 

6649 if not isinstance(beam_rays, list): 

6650 raise ValueError("Unexpected result from get_beam_rays_at_pixel.") 

6651 

6652 if len(beam_rays) < 2: 

6653 raise ValueError( 

6654 "At least two beam rays are required to compute an intersection." 

6655 ) 

6656 

6657 try: 

6658 video_xy = beam_rays[0]["video_xy"] 

6659 except Exception: 

6660 video_xy = self.pixel_to_video(x_pixel, y_pixel) 

6661 

6662 P0 = np.asarray([ray["origin"] for ray in beam_rays], dtype=float) 

6663 directions = np.asarray([ray["direction"] for ray in beam_rays], dtype=float) 

6664 P1 = P0 + directions 

6665 

6666 intersection = np.asarray( 

6667 intersection_point_multiple_lines(P0, P1), 

6668 dtype=float, 

6669 ).reshape(3) 

6670 

6671 distances = np.asarray( 

6672 [ 

6673 distance_point_line( 

6674 intersection[np.newaxis, :], 

6675 ray["origin"][np.newaxis, :], 

6676 ray["direction"][np.newaxis, :], 

6677 )[0] 

6678 for ray in beam_rays 

6679 ], 

6680 dtype=float, 

6681 ) 

6682 

6683 return { 

6684 "pixel_xy": (float(x_pixel), float(y_pixel)), 

6685 "video_xy": tuple(float(v) for v in video_xy), 

6686 "intersection": intersection, 

6687 "distances": distances, 

6688 "rms_distance": float(np.sqrt(np.mean(distances**2))), 

6689 "mean_distance": float(np.mean(distances)), 

6690 "max_distance": float(np.max(distances)), 

6691 "beam_rays": beam_rays, 

6692 } 

6693 

6694 def _get_measpoint_pixel_coordinates(self) -> np.ndarray: 

6695 """Return measurement-point pixel coordinates as an ``(n, 2)`` array.""" 

6696 image = self.get_image() 

6697 if isinstance(image, list): 

6698 raise ValueError( 

6699 "Combined files should be handled before calling " 

6700 "_get_measpoint_pixel_coordinates." 

6701 ) 

6702 

6703 video_rect = tuple(float(v) for v in self.measurement_points.get_video_rect()) 

6704 image_width, image_height = image.size 

6705 

6706 pixel_points: list[tuple[float, float]] = [] 

6707 for pt in self.measurement_points: 

6708 try: 

6709 x_video, y_video = pt.video_xy() 

6710 except Exception: 

6711 continue 

6712 pixel_points.append( 

6713 self._video_to_pixel( 

6714 float(x_video), 

6715 float(y_video), 

6716 video_rect, 

6717 image_width, 

6718 image_height, 

6719 ) 

6720 ) 

6721 

6722 if not pixel_points: 

6723 return np.empty((0, 2), dtype=float) 

6724 

6725 return np.asarray(pixel_points, dtype=float) 

6726 

6727 @staticmethod 

6728 def _make_convex_hull_path( 

6729 pixel_points: np.ndarray, 

6730 *, 

6731 buffer_pixels: float = 0.0, 

6732 ) -> Any: 

6733 """Create a matplotlib Path representing the convex hull of points. 

6734 

6735 Parameters 

6736 ---------- 

6737 pixel_points 

6738 Array of shape ``(n, 2)`` of pixel coordinates. 

6739 buffer_pixels 

6740 Optional outward buffer distance in pixels. If zero, the raw convex 

6741 hull polygon is used. 

6742 

6743 Returns 

6744 ------- 

6745 matplotlib.path.Path | None 

6746 Path for the hull polygon, or ``None`` if fewer than three points 

6747 are available. 

6748 """ 

6749 if pixel_points.shape[0] < 3: 

6750 return None 

6751 

6752 from scipy.spatial import ConvexHull 

6753 from matplotlib.path import Path 

6754 

6755 hull = ConvexHull(pixel_points) 

6756 hull_points = pixel_points[hull.vertices] 

6757 

6758 if buffer_pixels > 0.0: 

6759 centroid = hull_points.mean(axis=0) 

6760 shifted = hull_points - centroid 

6761 norms = np.linalg.norm(shifted, axis=1) 

6762 norms[norms == 0.0] = 1.0 

6763 hull_points = centroid + shifted * ( 

6764 (norms + float(buffer_pixels)) / norms 

6765 )[:, np.newaxis] 

6766 

6767 return Path(hull_points) 

6768 

6769 @staticmethod 

6770 def _grid_points_from_mesh( 

6771 xx: np.ndarray, 

6772 yy: np.ndarray, 

6773 ) -> np.ndarray: 

6774 """Convert meshgrid arrays into an ``(n, 2)`` array of point coordinates.""" 

6775 return np.column_stack((xx.ravel(), yy.ravel())) 

6776 

6777 def compute_alignment_error_map( 

6778 self, 

6779 *, 

6780 pixel_step: int = 10, 

6781 region_mode: str = "full", 

6782 hull_buffer_pixels: float = 0.0, 

6783 scale_region_mode: str = "measpoint_hull", 

6784 metric: str = "rms", 

6785 distance0: float = 0.0, 

6786 distance1: float = 1.0, 

6787 ) -> dict[str, Any] | list[Any]: 

6788 """Compute a sampled alignment-error field over the image. 

6789 

6790 Parameters 

6791 ---------- 

6792 pixel_step 

6793 Sampling stride in pixels in both x and y directions. 

6794 region_mode 

6795 Region over which to compute the field. Supported values are: 

6796 - ``"full"`` 

6797 - ``"measpoint_hull"`` 

6798 hull_buffer_pixels 

6799 Optional outward buffer applied to the measurement-point convex hull. 

6800 scale_region_mode 

6801 Region used to determine colormap scaling values. Supported values: 

6802 - ``"full"`` 

6803 - ``"measpoint_hull"`` 

6804 metric 

6805 Error metric extracted from ``get_beam_intersection_at_pixel``. 

6806 Supported values: 

6807 - ``"rms"`` 

6808 - ``"mean"`` 

6809 - ``"max"`` 

6810 distance0, distance1 

6811 Distances used to construct beam rays. 

6812 

6813 Returns 

6814 ------- 

6815 dict[str, Any] | list[Any] 

6816 For a normal file, returns a dictionary containing: 

6817 - ``x_grid`` 

6818 - ``y_grid`` 

6819 - ``error`` 

6820 - ``compute_mask`` 

6821 - ``scale_mask`` 

6822 - ``metric`` 

6823 - ``region_mode`` 

6824 - ``scale_region_mode`` 

6825 - ``pixel_step`` 

6826 - ``hull_points`` 

6827 - ``vmin`` 

6828 - ``vmax`` 

6829 

6830 For a combined file, returns a list of per-contained-file results. 

6831 """ 

6832 contained = self._get_contained_files_or_none() 

6833 if contained is not None: 

6834 return self._delegate_image_method_to_contained_files( 

6835 "compute_alignment_error_map", 

6836 pixel_step=pixel_step, 

6837 region_mode=region_mode, 

6838 hull_buffer_pixels=hull_buffer_pixels, 

6839 scale_region_mode=scale_region_mode, 

6840 metric=metric, 

6841 distance0=distance0, 

6842 distance1=distance1, 

6843 ) 

6844 

6845 metric_l = metric.lower() 

6846 if metric_l not in {"rms", "mean", "max"}: 

6847 raise ValueError("`metric` must be one of 'rms', 'mean', or 'max'.") 

6848 

6849 region_mode_l = region_mode.lower() 

6850 if region_mode_l not in {"full", "measpoint_hull"}: 

6851 raise ValueError("`region_mode` must be 'full' or 'measpoint_hull'.") 

6852 

6853 scale_region_mode_l = scale_region_mode.lower() 

6854 if scale_region_mode_l not in {"full", "measpoint_hull"}: 

6855 raise ValueError( 

6856 "`scale_region_mode` must be 'full' or 'measpoint_hull'." 

6857 ) 

6858 

6859 if pixel_step <= 0: 

6860 raise ValueError("`pixel_step` must be a positive integer.") 

6861 

6862 image = self.get_image() 

6863 if isinstance(image, list): 

6864 raise ValueError( 

6865 "Combined files should be handled before calling " 

6866 "compute_alignment_error_map." 

6867 ) 

6868 

6869 image_width, image_height = image.size 

6870 

6871 x_vals = np.arange(0, image_width, pixel_step, dtype=float) 

6872 y_vals = np.arange(0, image_height, pixel_step, dtype=float) 

6873 if len(x_vals) == 0 or x_vals[-1] != image_width - 1: 

6874 x_vals = np.append(x_vals, image_width - 1) 

6875 if len(y_vals) == 0 or y_vals[-1] != image_height - 1: 

6876 y_vals = np.append(y_vals, image_height - 1) 

6877 

6878 xx, yy = np.meshgrid(x_vals, y_vals) 

6879 error = np.full(xx.shape, np.nan, dtype=float) 

6880 

6881 measpoint_pixels = self._get_measpoint_pixel_coordinates() 

6882 hull_path = self._make_convex_hull_path( 

6883 measpoint_pixels, 

6884 buffer_pixels=hull_buffer_pixels, 

6885 ) 

6886 

6887 if hull_path is not None: 

6888 grid_points = self._grid_points_from_mesh(xx, yy) 

6889 hull_mask_flat = hull_path.contains_points(grid_points) 

6890 hull_mask = hull_mask_flat.reshape(xx.shape) 

6891 else: 

6892 hull_mask = np.zeros(xx.shape, dtype=bool) 

6893 

6894 if region_mode_l == "full": 

6895 compute_mask = np.ones(xx.shape, dtype=bool) 

6896 else: 

6897 if hull_path is None: 

6898 compute_mask = np.ones(xx.shape, dtype=bool) 

6899 else: 

6900 compute_mask = hull_mask 

6901 

6902 for iy in range(xx.shape[0]): 

6903 for ix in range(xx.shape[1]): 

6904 if not compute_mask[iy, ix]: 

6905 continue 

6906 

6907 x_pixel = float(xx[iy, ix]) 

6908 y_pixel = float(yy[iy, ix]) 

6909 

6910 try: 

6911 result = self.get_beam_intersection_at_pixel( 

6912 x_pixel, 

6913 y_pixel, 

6914 distance0=distance0, 

6915 distance1=distance1, 

6916 ) 

6917 if metric_l == "rms": 

6918 value = result["rms_distance"] 

6919 elif metric_l == "mean": 

6920 value = result["mean_distance"] 

6921 else: 

6922 value = result["max_distance"] 

6923 error[iy, ix] = float(value) 

6924 except Exception: 

6925 error[iy, ix] = np.nan 

6926 

6927 if scale_region_mode_l == "full": 

6928 scale_mask = np.isfinite(error) 

6929 else: 

6930 if hull_path is None: 

6931 scale_mask = np.isfinite(error) 

6932 else: 

6933 scale_mask = hull_mask & np.isfinite(error) 

6934 

6935 if not np.any(scale_mask): 

6936 vmin = np.nan 

6937 vmax = np.nan 

6938 else: 

6939 scale_values = error[scale_mask] 

6940 vmin = float(np.nanmin(scale_values)) 

6941 vmax = float(np.nanmax(scale_values)) 

6942 

6943 hull_points = None 

6944 if hull_path is not None: 

6945 hull_points = np.asarray(hull_path.vertices, dtype=float) 

6946 

6947 return { 

6948 "x_grid": xx, 

6949 "y_grid": yy, 

6950 "error": error, 

6951 "compute_mask": compute_mask, 

6952 "scale_mask": scale_mask, 

6953 "metric": metric_l, 

6954 "region_mode": region_mode_l, 

6955 "scale_region_mode": scale_region_mode_l, 

6956 "pixel_step": int(pixel_step), 

6957 "hull_points": hull_points, 

6958 "vmin": vmin, 

6959 "vmax": vmax, 

6960 } 

6961 

6962 def get_alignment_error_image( 

6963 self, 

6964 *, 

6965 pixel_step: int = 10, 

6966 region_mode: str = "full", 

6967 hull_buffer_pixels: float = 0.0, 

6968 scale_region_mode: str = "measpoint_hull", 

6969 metric: str = "max", 

6970 distance0: float = 0.0, 

6971 distance1: float = 1.0, 

6972 cmap: str = "viridis", 

6973 alpha: float = 0.6, 

6974 show_measurement_points: bool = False, 

6975 show_hull: bool = False, 

6976 ax: Any = None, 

6977 title: str | None = None, 

6978 ) -> Any: 

6979 """Overlay a sampled alignment-error colormap on the stored image. 

6980 

6981 Parameters 

6982 ---------- 

6983 pixel_step 

6984 Sampling stride in pixels. 

6985 region_mode 

6986 Region over which to compute the field: 

6987 - ``"full"`` 

6988 - ``"measpoint_hull"`` 

6989 hull_buffer_pixels 

6990 Optional outward buffer applied to the measurement-point convex hull. 

6991 scale_region_mode 

6992 Region used to set colormap scaling: 

6993 - ``"full"`` 

6994 - ``"measpoint_hull"`` 

6995 metric 

6996 Error metric to display: 

6997 - ``"rms"`` 

6998 - ``"mean"`` 

6999 - ``"max"`` 

7000 distance0, distance1 

7001 Distances used to construct beam rays. 

7002 cmap 

7003 Matplotlib colormap name. 

7004 alpha 

7005 Overlay transparency. 

7006 show_measurement_points 

7007 Whether to overlay measurement-point positions. 

7008 show_hull 

7009 Whether to draw the convex hull polygon used for hull-based masking. 

7010 ax 

7011 Existing matplotlib axes to draw onto. 

7012 title 

7013 Plot title. If omitted, a default title is generated. 

7014 

7015 Returns 

7016 ------- 

7017 Any 

7018 For a normal file, returns ``(fig, ax, result_dict)``. 

7019 For a combined file, returns a list of per-contained-file results. 

7020 """ 

7021 contained = self._get_contained_files_or_none() 

7022 if contained is not None: 

7023 return self._delegate_image_method_to_contained_files( 

7024 "get_alignment_error_image", 

7025 pixel_step=pixel_step, 

7026 region_mode=region_mode, 

7027 hull_buffer_pixels=hull_buffer_pixels, 

7028 scale_region_mode=scale_region_mode, 

7029 metric=metric, 

7030 distance0=distance0, 

7031 distance1=distance1, 

7032 cmap=cmap, 

7033 alpha=alpha, 

7034 show_measurement_points=show_measurement_points, 

7035 show_hull=show_hull, 

7036 ax=ax, 

7037 title=title, 

7038 ) 

7039 

7040 image = self.get_image() 

7041 if isinstance(image, list): 

7042 raise ValueError( 

7043 "Combined files should be handled before calling " 

7044 "get_alignment_error_image." 

7045 ) 

7046 

7047 result = self.compute_alignment_error_map( 

7048 pixel_step=pixel_step, 

7049 region_mode=region_mode, 

7050 hull_buffer_pixels=hull_buffer_pixels, 

7051 scale_region_mode=scale_region_mode, 

7052 metric=metric, 

7053 distance0=distance0, 

7054 distance1=distance1, 

7055 ) 

7056 if isinstance(result, list): 

7057 raise ValueError("Unexpected combined-file result during plotting.") 

7058 

7059 xx = result["x_grid"] 

7060 yy = result["y_grid"] 

7061 error = result["error"] 

7062 vmin = result["vmin"] 

7063 vmax = result["vmax"] 

7064 hull_points = result["hull_points"] 

7065 

7066 created_fig = False 

7067 if ax is None: 

7068 fig, ax = plt.subplots() 

7069 created_fig = True 

7070 else: 

7071 fig = ax.figure 

7072 

7073 ax.imshow(image) 

7074 

7075 mesh = ax.pcolormesh( 

7076 xx, 

7077 yy, 

7078 error, 

7079 shading="nearest", 

7080 cmap=cmap, 

7081 alpha=alpha, 

7082 vmin=None if np.isnan(vmin) else vmin, 

7083 vmax=None if np.isnan(vmax) else vmax, 

7084 ) 

7085 cbar = fig.colorbar(mesh, ax=ax) 

7086 cbar.set_label(f"{result['metric']} beam intersection error") 

7087 

7088 if show_measurement_points: 

7089 measpoint_pixels = self._get_measpoint_pixel_coordinates() 

7090 if measpoint_pixels.size > 0: 

7091 ax.scatter( 

7092 measpoint_pixels[:, 0], 

7093 measpoint_pixels[:, 1], 

7094 c="white", 

7095 edgecolors="black", 

7096 s=20, 

7097 marker="o", 

7098 label="Measurement Points", 

7099 ) 

7100 

7101 if show_hull and hull_points is not None and len(hull_points) > 0: 

7102 closed_hull = np.vstack((hull_points, hull_points[0])) 

7103 ax.plot( 

7104 closed_hull[:, 0], 

7105 closed_hull[:, 1], 

7106 color="white", 

7107 linestyle="--", 

7108 linewidth=1.5, 

7109 label="Measurement-Point Hull", 

7110 ) 

7111 

7112 image_width, image_height = image.size 

7113 ax.set_xlim(0, image_width) 

7114 ax.set_ylim(image_height, 0) 

7115 ax.set_aspect("equal") 

7116 ax.set_xlabel("Pixel X") 

7117 ax.set_ylabel("Pixel Y") 

7118 

7119 if title is None: 

7120 title = ( 

7121 f"Alignment Error Map ({result['metric']}, step={result['pixel_step']}, " 

7122 f"region={result['region_mode']}, scale={result['scale_region_mode']})" 

7123 ) 

7124 ax.set_title(title) 

7125 

7126 if show_measurement_points or (show_hull and hull_points is not None): 

7127 ax.legend() 

7128 

7129 if created_fig: 

7130 fig.tight_layout() 

7131 

7132 return fig, ax, result