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
« 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.
5This module provides a Python wrapper around the Polytec ``Polyfile`` COM
6interface, along with convenience methods for extracting common data products
7into ``sdynpy`` objects.
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"""
17from __future__ import annotations
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.
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.
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.
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"""
39from enum import Enum, IntFlag
40from io import BytesIO
41from os import PathLike
42from typing import Any, TypeVar
43from collections import defaultdict
45import numpy as np
46from PIL import Image, ImageDraw
47import matplotlib.pyplot as plt
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
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
79VERBOSE = False
81_FlagT = TypeVar("_FlagT", bound=IntFlag)
84class AcquisitionMode(Enum):
85 """Measurement acquisition mode."""
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
97class AverageType(Enum):
98 """Averaging mode."""
100 OFF = 0
101 MAGNITUDE = 1
102 COMPLEX = 2
103 TIME = 3
104 PEAK_HOLD = 4
107class DigitalFilterQuality(Enum):
108 """Quality setting for digital filters."""
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
121class DigitalFilterType(Enum):
122 """Digital filter type."""
124 LOWPASS = 0
125 HIGHPASS = 1
126 BANDPASS = 2
127 NOTCH = 3
128 NONE = 4
131class Direction(Enum):
132 """Measurement direction."""
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
143class Coupling(Enum):
144 """Input coupling mode."""
146 AC = 0
147 DC = 1
148 UNKNOWN = 2
151class PhysicalQuantity(Enum):
152 """Physical quantity identifier."""
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
183class ChannelType(Enum):
184 """Acquisition channel type."""
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
197class WindowFunction(Enum):
198 """Window function used for time/FFT processing."""
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
211class BandwidthExtension(Enum):
212 """Bandwidth-extension setting."""
214 NONE = 0
215 OFF = 1
216 ON1 = 2
217 ON2 = 3
220class WaveformType(Enum):
221 """Signal-generator waveform type."""
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
238class MultiFrameMode(Enum):
239 """Multi-frame acquisition mode."""
241 AUTOMATIC = 0
242 REGULAR = 1
243 MANUAL = 2
246class SignalEnhancementMode(Enum):
247 """Signal enhancement (SE) mode."""
249 FAST = 0
250 FAST_STANDARD = 1
251 STANDARD = 2
252 STANDARD_BEST = 3
253 BEST = 4
256class TriggerEdge(Enum):
257 """Trigger edge polarity."""
259 RISING = 0
260 FALLING = 1
263class TriggerSource(Enum):
264 """Trigger source."""
266 OFF = 0
267 EXTERNAL = 1
268 ANALOG = 2
269 INTERNAL = 3
272class ControllerCaps(IntFlag):
273 """Controller capability flags."""
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
294def decode_controller_caps(caps_value: int) -> ControllerCaps:
295 """Decode an integer bitmask into :class:`ControllerCaps`."""
296 return ControllerCaps(int(caps_value))
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)]
305class SensorHeadCaps(IntFlag):
306 """Sensor-head capability flags."""
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
343class SensorHeadCaps2(IntFlag):
344 """Extended sensor-head capability flags."""
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
360class SignalBalanceState(Enum):
361 """Signal-balance state."""
363 SEEK = 0
364 MINUS3 = 1
365 MINUS2 = 2
366 MINUS1 = 3
367 BALANCED = 4
368 PLUS1 = 5
369 PLUS2 = 6
370 PLUS3 = 7
373class FilterType(Enum):
374 """Filter type used in controller/filter settings."""
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
393class QuantityType(Enum):
394 """Quantity type used in controller quantity settings."""
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
434class AlignmentPoint3DCaps(IntFlag):
435 """3D alignment-point capability flags."""
437 COORD_3D = 1
438 DISTANCE = 2
439 QUALITY = 4
440 SCANNER = 8
441 VIDEO = 16
444class AlignmentPoint3DType(Enum):
445 """3D alignment-point type."""
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
462class CoordDefinitionMode(Enum):
463 """3D coordinate-definition mode."""
465 OFF = 0
466 ORIG_POINT_ON_AXIS_POINT_ON_PLANE = 1
467 THREE_POINTS_ON_AXES = 2
468 FREE_POINTS = 3
471class ScanHeadType(Enum):
472 """Scanning-head type."""
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
504class ScanStatus(IntFlag):
505 """Scan-point status flags."""
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
524class FrontEndCaps(IntFlag):
525 """Front-end capability flags."""
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
545class AcqBoard(Enum):
546 """Acquisition-board type."""
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
589class FrontEndType(Enum):
590 """Front-end type."""
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
747class GeneratorType(Enum):
748 """Signal-generator type."""
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
771class GeometryStatus(IntFlag):
772 """3D geometry status flags."""
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
791class FocusStatus(IntFlag):
792 """Laser-focus status flags."""
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
805class MeasurementLocationMode(Enum):
806 """Measurement-location mode."""
808 FIXED = 0
809 VARYING = 1
812class ProfileType(Enum):
813 """Profile type."""
815 UNKNOWN = 0
816 POLYGON = 1
817 HORIZONTAL = 2
818 VERTICAL = 4
819 ORDER = 8
822class DistanceSensorType(Enum):
823 """Distance-sensor type."""
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
835class PreferredScanDirection(Enum):
836 """Preferred scan direction."""
838 NONE = 0
839 X = 1
840 Y = 2
841 AUTO = 3
842 INDEX = 4
845class ScanHeadVideoType(IntFlag):
846 """Scanning-head video capability/type flags."""
848 NONE = 1
849 REMOTE = 2
850 COLOR = 4
851 PAL = 8
852 NTSC = 16
853 DIGITAL = 32
854 HD = 64
857class ScanHeadCaps(IntFlag):
858 """Scanning-head capability flags."""
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
878class GraphicFormatType(Enum):
879 """Graphics export format."""
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
896class LensCalibrationCaps(IntFlag):
897 """Lens-calibration capability flags."""
899 INTERFERENCE = 1
902class ChannelCaps(IntFlag):
903 """Signal channel capability flags."""
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
916class DomainType(Enum):
917 """Signal domain type."""
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
928class DisplayType(Enum):
929 """Display type for a signal."""
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
949class DataType(Enum):
950 """Signal data type."""
952 UNKNOWN = 0
953 POINT = 1
954 AVERAGE = 2
955 BAND = 3
958class DOFDirection(Enum):
959 """Degree-of-freedom direction."""
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
977class FunctionType(Enum):
978 """Signal function type."""
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
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
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
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
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}.")
1065class AverageProperties:
1066 """Wrapper around COM average settings."""
1068 def __init__(self, obj: Any) -> None:
1069 self._com = obj
1071 @property
1072 def count(self) -> int:
1073 """Number of averages in the measurement."""
1074 return int(self._com.Count)
1076 @property
1077 def type(self) -> AverageType:
1078 """Averaging type used for the measurement."""
1079 return AverageType(self._com.Type)
1082class DigitalFilter:
1083 """Wrapper around COM digital filter settings."""
1085 def __init__(self, obj: Any) -> None:
1086 self._com = obj
1088 @property
1089 def type(self) -> DigitalFilterType:
1090 """Filter type."""
1091 return DigitalFilterType(self._com.Type)
1093 @property
1094 def quality(self) -> DigitalFilterQuality:
1095 """Filter quality setting."""
1096 return DigitalFilterQuality(self._com.Quality)
1098 @property
1099 def cutoff_freq(self) -> float | tuple[float, float]:
1100 """Cutoff frequency or frequencies for the configured filter.
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.
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.")
1121class ChannelsProperties:
1122 """Wrapper around COM acquisition channel properties."""
1124 def __init__(self, obj: Any) -> None:
1125 self._com = obj
1127 @property
1128 def active(self) -> bool:
1129 """Whether the channel is active."""
1130 return bool(self._com.Active)
1132 @property
1133 def calibration(self) -> float:
1134 """Calibration factor in SI units."""
1135 return float(self._com.Calibration)
1137 @property
1138 def differential_input(self) -> bool:
1139 """Whether the channel expects differential input."""
1140 return bool(self._com.DifferentialInput)
1142 @property
1143 def digital_filter(self) -> DigitalFilter:
1144 """Digital filter applied to the input signal."""
1145 return DigitalFilter(self._com.DigitalFilter)
1147 @property
1148 def direction(self) -> Direction:
1149 """Direction of vibration for this channel."""
1150 return Direction(self._com.DirectionOfVibration)
1152 @property
1153 def icp_input(self) -> bool:
1154 """Whether the channel is configured for an ICP device."""
1155 return bool(self._com.ICPInput)
1157 @property
1158 def coupling(self) -> Coupling:
1159 """Input coupling for the channel."""
1160 return Coupling(self._com.InputCoupling)
1162 @property
1163 def impedance(self) -> float:
1164 """Input impedance in ohms."""
1165 return float(self._com.InputImpedance)
1167 @property
1168 def range(self) -> float:
1169 """Input range in volts."""
1170 return float(self._com.InputRange)
1172 @property
1173 def int_diff_physical_quantity(self) -> PhysicalQuantity:
1174 """Quantity produced by integration/differentiation filtering."""
1175 return PhysicalQuantity(self._com.IntDiffPhysicalQuantity)
1177 @property
1178 def name(self) -> str:
1179 """Channel descriptor."""
1180 return self._com.Name
1182 @property
1183 def quantity(self) -> PhysicalQuantity:
1184 """Physical quantity of the channel."""
1185 return PhysicalQuantity(self._com.Quantity)
1187 @property
1188 def reference(self) -> bool:
1189 """Whether the channel is marked as a reference."""
1190 return bool(self._com.Reference)
1192 @property
1193 def node_id(self) -> int:
1194 """One-based scan-point index associated with the channel."""
1195 return int(self._com.ReferencePointIndex)
1197 @property
1198 def signal_enhancement(self) -> bool:
1199 """Whether signal enhancement is active for the channel."""
1200 return bool(self._com.SEActive)
1202 @property
1203 def short_name(self) -> str:
1204 """Abbreviated channel descriptor."""
1205 return self._com.ShortName
1207 @property
1208 def signal_delay(self) -> float:
1209 """Signal delay in seconds."""
1210 return float(self._com.SignalDelay)
1212 @property
1213 def type(self) -> ChannelType:
1214 """Signal type of the channel."""
1215 return ChannelType(self._com.Type)
1217 @property
1218 def unit(self) -> str:
1219 """Engineering unit of the channel quantity."""
1220 return self._com.Unit
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)
1227 @property
1228 def window_function(self) -> WindowFunction:
1229 """Window function used for the channel."""
1230 return WindowFunction(self._com.WindowFunction)
1232 @property
1233 def window_function_parameters(self) -> Any:
1234 """Parameters of the configured window function."""
1235 return self._com.WindowFunctionParams
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)
1243class FastScanProperties:
1244 """Wrapper around COM FastScan acquisition properties."""
1246 def __init__(self, obj: Any) -> None:
1247 self._com = obj
1249 @property
1250 def bandwidth(self) -> float:
1251 """FastScan bandwidth in hertz."""
1252 return float(self._com.Bandwidth)
1254 @property
1255 def frequency(self) -> float:
1256 """FastScan frequency in hertz."""
1257 return float(self._com.Frequency)
1259 @property
1260 def sample_frequency(self) -> float:
1261 """Sampling frequency in hertz."""
1262 return float(self._com.SampleFrequency)
1264 @property
1265 def samples(self) -> int:
1266 """Number of samples in the FastScan acquisition."""
1267 return int(self._com.Samples)
1269 @property
1270 def sample_time(self) -> float:
1271 """Sample time in seconds."""
1272 return float(self._com.SampleTime)
1275class FFTProperties:
1276 """Wrapper around COM FFT acquisition properties."""
1278 def __init__(self, obj: Any) -> None:
1279 self._com = obj
1281 @property
1282 def bandwidth(self) -> float:
1283 """FFT bandwidth in hertz."""
1284 return float(self._com.Bandwidth)
1286 @property
1287 def end_frequency(self) -> float:
1288 """End frequency in hertz."""
1289 return float(self._com.EndFrequency)
1291 @property
1292 def lines(self) -> int:
1293 """Number of FFT lines."""
1294 return int(self._com.Lines)
1296 @property
1297 def overlap(self) -> float:
1298 """Overlap in percent."""
1299 return float(self._com.Overlap)
1301 @property
1302 def sample_frequency(self) -> float:
1303 """Sampling frequency in hertz."""
1304 return float(self._com.SampleFrequency)
1306 @property
1307 def sample_resolution(self) -> float:
1308 """Frequency resolution in hertz."""
1309 return float(self._com.SampleResolution)
1311 @property
1312 def samples(self) -> int:
1313 """Number of samples used for the FFT."""
1314 return int(self._com.Samples)
1316 @property
1317 def sample_time(self) -> float:
1318 """Sample time in seconds."""
1319 return float(self._com.SampleTime)
1321 @property
1322 def start_frequency(self) -> float:
1323 """Start frequency in hertz."""
1324 return float(self._com.StartFrequency)
1327class FrontEndProperties:
1328 """Wrapper around COM front-end acquisition properties."""
1330 def __init__(self, obj: Any) -> None:
1331 self._com = obj
1333 @property
1334 def generator_amplifier_enabled(self) -> bool:
1335 """Whether the generator amplifier is enabled."""
1336 return bool(self._com.GeneratorAmplifierEnabled)
1338 @property
1339 def generator_amplifier_factor(self) -> float:
1340 """Amplification factor of the generator amplifier."""
1341 return float(self._com.GeneratorAmplifierFactor)
1344class GeneralProperties:
1345 """Wrapper around COM general acquisition properties."""
1347 def __init__(self, obj: Any) -> None:
1348 self._com = obj
1350 @property
1351 def auto_remeasure(self) -> bool:
1352 """Whether auto remeasure is enabled."""
1353 return bool(self._com.AutoRemeasure)
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)
1360 @property
1361 def bandwidth_extension(self) -> BandwidthExtension:
1362 """Bandwidth extension setting for the UHF vibrometer."""
1363 return BandwidthExtension(self._com.BandWidthExtension)
1365 @property
1366 def principal_component_analysis(self) -> bool:
1367 """Whether principal component analysis (MIMO) is enabled."""
1368 return bool(self._com.PrincipalComponentAnalysis)
1370 @property
1371 def real_sample_frequency(self) -> float:
1372 """Raw sampling frequency before digital decimation."""
1373 return float(self._com.RealSampleFrequency)
1376class GeneratorsProperties:
1377 """Wrapper around COM signal-generator properties."""
1379 def __init__(self, obj: Any) -> None:
1380 self._com = obj
1382 @property
1383 def active(self) -> bool:
1384 """Whether the generator is active."""
1385 return bool(self._com.Active)
1387 @property
1388 def amplitude(self) -> float:
1389 """Generator amplitude in volts."""
1390 return float(self._com.Amplitude)
1392 @property
1393 def amplitude_correction_data(self) -> Any:
1394 """Amplitude-correction data."""
1395 return self._com.AmplitudeCorrectionData
1397 @property
1398 def multiple_channels(self) -> bool:
1399 """Whether the generator uses multiple output channels."""
1400 return bool(self._com.MultipleChannels)
1402 @property
1403 def offset(self) -> float:
1404 """Generator DC offset in volts."""
1405 return float(self._com.Offset)
1407 @property
1408 def steady_state_time(self) -> float:
1409 """Waiting time for steady-state conditions in seconds."""
1410 return float(self._com.SteadyStateTime)
1412 @property
1413 def supports_multiple_channels(self) -> bool:
1414 """Whether the generator supports multiple output channels."""
1415 return bool(self._com.SupportsMultipleChannels)
1417 @property
1418 def use_amplitude_correction(self) -> bool:
1419 """Whether amplitude-correction data is used."""
1420 return bool(self._com.UseAmplitudeCorrection)
1422 @property
1423 def waveform(self) -> "Waveform":
1424 """Configured waveform."""
1425 return make_waveform(self._com.Waveform)
1428class Waveform:
1429 """Base wrapper for generator waveform settings."""
1431 def __init__(self, obj: Any) -> None:
1432 self._com = obj
1434 @property
1435 def name(self) -> str:
1436 """Waveform name."""
1437 return self._com.Name
1439 @property
1440 def type(self) -> WaveformType:
1441 """Waveform type."""
1442 return WaveformType(self._com.Type)
1445class BurstChirpWaveform(Waveform):
1446 """Burst-chirp waveform settings."""
1448 @property
1449 def burst_length(self) -> float:
1450 """Burst length in percent."""
1451 return float(self._com.BurstLength)
1453 @property
1454 def burst_length_ex(self) -> float:
1455 """Extended burst length in percent."""
1456 return float(self._com.BurstLengthEx)
1458 @property
1459 def burst_start(self) -> float:
1460 """Burst start in percent."""
1461 return float(self._com.BurstStart)
1463 @property
1464 def burst_start_ex(self) -> float:
1465 """Extended burst start in percent."""
1466 return float(self._com.BurstStartEx)
1468 @property
1469 def end_frequency(self) -> float:
1470 """End frequency in hertz."""
1471 return float(self._com.EndFrequency)
1473 @property
1474 def start_frequency(self) -> float:
1475 """Start frequency in hertz."""
1476 return float(self._com.StartFrequency)
1479class BurstRandomWaveform(Waveform):
1480 """Burst-random waveform settings."""
1482 @property
1483 def burst_length(self) -> float:
1484 """Burst length in percent."""
1485 return float(self._com.BurstLength)
1487 @property
1488 def burst_length_ex(self) -> float:
1489 """Extended burst length in percent."""
1490 return float(self._com.BurstLengthEx)
1492 @property
1493 def burst_start(self) -> float:
1494 """Burst start in percent."""
1495 return float(self._com.BurstStart)
1497 @property
1498 def burst_start_ex(self) -> float:
1499 """Extended burst start in percent."""
1500 return float(self._com.BurstStartEx)
1503class MultiCarrierCWWaveform(Waveform):
1504 """Multi-carrier continuous-wave waveform settings."""
1506 @property
1507 def end_frequency(self) -> float:
1508 """End frequency in hertz."""
1509 return float(self._com.EndFrequency)
1511 @property
1512 def start_frequency(self) -> float:
1513 """Start frequency in hertz."""
1514 return float(self._com.StartFrequency)
1517class PeriodicChirpWaveform(Waveform):
1518 """Periodic-chirp waveform."""
1521class PseudoRandomWaveform(Waveform):
1522 """Pseudo-random waveform."""
1525class RampWaveform(Waveform):
1526 """Ramp waveform settings."""
1528 @property
1529 def frequency(self) -> float:
1530 """Waveform frequency in hertz."""
1531 return float(self._com.Frequency)
1534class SineWaveform(Waveform):
1535 """Sine waveform settings."""
1537 @property
1538 def frequency(self) -> float:
1539 """Waveform frequency in hertz."""
1540 return float(self._com.Frequency)
1543class SquareWaveform(Waveform):
1544 """Square waveform settings."""
1546 @property
1547 def frequency(self) -> float:
1548 """Waveform frequency in hertz."""
1549 return float(self._com.Frequency)
1552class SweepWaveform(Waveform):
1553 """Sweep waveform settings."""
1555 @property
1556 def end_frequency(self) -> float:
1557 """End frequency of the sweep in hertz."""
1558 return float(self._com.EndFrequency)
1560 @property
1561 def start_frequency(self) -> float:
1562 """Start frequency of the sweep in hertz."""
1563 return float(self._com.StartFrequency)
1565 @property
1566 def sweep_time(self) -> float:
1567 """Sweep duration in seconds."""
1568 return float(self._com.SweepTime)
1571class TriangleWaveform(Waveform):
1572 """Triangle waveform settings."""
1574 @property
1575 def frequency(self) -> float:
1576 """Waveform frequency in hertz."""
1577 return float(self._com.Frequency)
1580class WhiteNoiseWaveform(Waveform):
1581 """White-noise waveform."""
1584class UserDefinedWaveform(Waveform):
1585 """User-defined waveform settings."""
1587 @property
1588 def frequency(self) -> float:
1589 """Waveform frequency in hertz."""
1590 return float(self._com.Frequency)
1592 @property
1593 def waveform_data(self) -> Any:
1594 """Waveform data for the user-defined waveform."""
1595 return self._com.WaveformData
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)
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 }
1617 cls = cls_map.get(wf_type, Waveform)
1618 return cls(waveform_com_obj)
1621class MultiFrameProperties:
1622 """Wrapper around COM multi-frame acquisition properties."""
1624 def __init__(self, obj: Any) -> None:
1625 self._com = obj
1627 @property
1628 def average_type(self) -> AverageType:
1629 """Average type used to average frames."""
1630 return AverageType(self._com.AverageType)
1632 @property
1633 def frame_count(self) -> int:
1634 """Number of frames."""
1635 return int(self._com.FrameCount)
1637 @property
1638 def mode(self) -> MultiFrameMode:
1639 """Multi-frame mode."""
1640 return MultiFrameMode(self._com.Mode)
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)
1648class SignalEnhancementProperties:
1649 """Wrapper around COM signal-enhancement properties."""
1651 def __init__(self, obj: Any) -> None:
1652 self._com = obj
1654 @property
1655 def mode(self) -> SignalEnhancementMode:
1656 """Signal-enhancement mode."""
1657 return SignalEnhancementMode(self._com.Mode)
1659 @property
1660 def speckle_tracking(self) -> bool:
1661 """Whether speckle tracking is active."""
1662 return bool(self._com.SpeckleTracking)
1665class TimeProperties:
1666 """Wrapper around COM time-acquisition properties."""
1668 def __init__(self, obj: Any) -> None:
1669 self._com = obj
1671 @property
1672 def sample_frequency(self) -> float:
1673 """Sample frequency in hertz."""
1674 return float(self._com.SampleFrequency)
1676 @property
1677 def sample_resolution(self) -> float:
1678 """Time resolution in seconds."""
1679 return float(self._com.SampleResolution)
1681 @property
1682 def samples(self) -> int:
1683 """Number of samples."""
1684 return int(self._com.Samples)
1686 @property
1687 def sample_time(self) -> float:
1688 """Sample time in seconds."""
1689 return float(self._com.SampleTime)
1692class TriggerProperties:
1693 """Wrapper around COM trigger properties."""
1695 def __init__(self, obj: Any) -> None:
1696 self._com = obj
1698 @property
1699 def analog_source_channel(self) -> int:
1700 """Source channel for analog triggering."""
1701 return int(self._com.AnalogSourceChannel)
1703 @property
1704 def edge(self) -> TriggerEdge:
1705 """Trigger edge."""
1706 return TriggerEdge(self._com.Edge)
1708 @property
1709 def level(self) -> float:
1710 """Trigger level in percent."""
1711 return float(self._com.Level)
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)
1718 @property
1719 def pre_trigger(self) -> float:
1720 """Pre-trigger value in percent."""
1721 return float(self._com.PreTrigger)
1723 @property
1724 def pre_trigger_max(self) -> float:
1725 """Maximum pre-trigger value in percent."""
1726 return float(self._com.PreTriggerMax)
1728 @property
1729 def pre_trigger_min(self) -> float:
1730 """Minimum pre-trigger value in percent."""
1731 return float(self._com.PreTriggerMin)
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)
1738 @property
1739 def source(self) -> TriggerSource:
1740 """Trigger source."""
1741 return TriggerSource(self._com.Source)
1744class ControllerInfo:
1745 """Wrapper around COM controller-information settings."""
1747 def __init__(self, obj: Any) -> None:
1748 self._com = obj
1750 @property
1751 def caps_value(self) -> int:
1752 """Controller capabilities as a raw integer bitmask."""
1753 return int(self._com.Caps)
1755 @property
1756 def caps(self) -> ControllerCaps:
1757 """Controller capabilities as an ``IntFlag``."""
1758 return decode_controller_caps(self.caps_value)
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)
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)
1769 @property
1770 def device_name(self) -> str:
1771 """General controller device name."""
1772 return self._com.DeviceName
1774 @property
1775 def firmware_version(self) -> str:
1776 """Controller firmware version."""
1777 return self._com.FirmwareVersion
1779 @property
1780 def name(self) -> str:
1781 """Controller type/name."""
1782 return self._com.Name
1784 @property
1785 def serial_number(self) -> str:
1786 """Controller serial number."""
1787 return self._com.SerialNumber
1789 @property
1790 def unique_id(self) -> str:
1791 """Controller unique identifier."""
1792 return self._com.UniqueID
1795class VideoCameraSettings:
1796 """Wrapper around COM video-camera settings."""
1798 def __init__(self, obj: Any) -> None:
1799 self._com = obj
1801 @property
1802 def automatic_gain_control(self) -> bool:
1803 """Whether automatic gain control is active."""
1804 return bool(self._com.AutomaticGainControl)
1806 @property
1807 def gamma(self) -> float:
1808 """Gamma value."""
1809 return float(self._com.Gamma)
1811 @property
1812 def manual_gain(self) -> float:
1813 """Manual gain value."""
1814 return float(self._com.ManualGain)
1816 @property
1817 def mirror(self) -> bool:
1818 """Mirror mode state."""
1819 return bool(self._com.Mirror)
1821 @property
1822 def shutter_speed(self) -> float:
1823 """Shutter speed."""
1824 return float(self._com.ShutterSpeed)
1827class SensorHeadInfo:
1828 """Wrapper around COM sensor-head information."""
1830 def __init__(self, obj: Any) -> None:
1831 self._com = obj
1833 @property
1834 def beam_gap(self) -> Any:
1835 """Beam gap, including units when provided by the COM interface."""
1836 return self._com.BeamGap
1838 @property
1839 def bragg_cell_frequency(self) -> float:
1840 """Bragg-cell frequency."""
1841 return float(self._com.BraggCellFrequency)
1843 @property
1844 def caps_value(self) -> int:
1845 """Sensor-head capabilities as a raw integer bitmask."""
1846 return int(self._com.Caps)
1848 @property
1849 def caps(self) -> SensorHeadCaps:
1850 """Sensor-head capabilities as an ``IntFlag``."""
1851 return SensorHeadCaps(self.caps_value)
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)
1858 @property
1859 def caps2_value(self) -> int:
1860 """Extended sensor-head capabilities as a raw integer bitmask."""
1861 return int(self._com.Caps2)
1863 @property
1864 def caps2(self) -> SensorHeadCaps2:
1865 """Extended sensor-head capabilities as an ``IntFlag``."""
1866 return SensorHeadCaps2(self.caps2_value)
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)
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)
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)
1881 @property
1882 def coherence_optimizer_firmware_version(self) -> str:
1883 """Firmware version of the coherence optimizer."""
1884 return self._com.CoherenceOptimizerFirmwareVersion
1886 @property
1887 def connected(self) -> bool:
1888 """Whether the sensor head is connected."""
1889 return bool(self._com.Connected)
1891 @property
1892 def dimmer_max(self) -> float:
1893 """Maximum laser-beam transmission."""
1894 return float(self._com.DimmerMax)
1896 @property
1897 def dimmer_min(self) -> float:
1898 """Minimum laser-beam transmission."""
1899 return float(self._com.DimmerMin)
1901 @property
1902 def firmware_version(self) -> str:
1903 """Sensor-head firmware version."""
1904 return self._com.FirmwareVersion
1906 @property
1907 def focus_far_limit(self) -> float:
1908 """Farthest focus position."""
1909 return float(self._com.FocusFarLimit)
1911 @property
1912 def focus_near_limit(self) -> float:
1913 """Nearest focus position."""
1914 return float(self._com.FocusNearLimit)
1916 @property
1917 def illumination_intensity_max(self) -> float:
1918 """Maximum illumination intensity."""
1919 return float(self._com.IlluminationIntensityMax)
1921 @property
1922 def illumination_intensity_min(self) -> float:
1923 """Minimum illumination intensity."""
1924 return float(self._com.IlluminationIntensityMin)
1926 @property
1927 def laser_delay(self) -> float:
1928 """Laser delay in seconds."""
1929 return float(self._com.LaserDelay)
1931 @property
1932 def laser_firmware_version(self) -> str:
1933 """Laser firmware version string."""
1934 return self._com.LaserFirmwareVersion
1936 @property
1937 def laser_power(self) -> float:
1938 """Laser power in milliwatts."""
1939 return float(self._com.LaserPower)
1941 @property
1942 def laser_wavelength(self) -> float:
1943 """Laser wavelength in nanometers."""
1944 return float(self._com.LaserWavelength)
1946 @property
1947 def lower_objective_threshold_temperature(self) -> float:
1948 """Lower objective-threshold temperature."""
1949 return float(self._com.LowerObjectiveThresholdTemperature)
1951 @property
1952 def max_rgb_led_color_index(self) -> int:
1953 """Maximum RGB LED color index."""
1954 return int(self._com.MaxRGBLedColorIndex)
1956 @property
1957 def max_signal_balance(self) -> SignalBalanceState:
1958 """Upper end of the signal-balance range."""
1959 return SignalBalanceState(self._com.MaxSignalBalance)
1961 @property
1962 def min_rgb_led_color_index(self) -> int:
1963 """Minimum RGB LED color index."""
1964 return int(self._com.MinRGBLedColorIndex)
1966 @property
1967 def min_signal_balance(self) -> SignalBalanceState:
1968 """Lower end of the signal-balance range."""
1969 return SignalBalanceState(self._com.MinSignalBalance)
1971 @property
1972 def name(self) -> str:
1973 """Sensor-head name."""
1974 return self._com.Name
1976 @property
1977 def serial_number(self) -> str:
1978 """Sensor-head serial number."""
1979 return self._com.SerialNumber
1981 @property
1982 def sonde_name(self) -> str:
1983 """Sonde name."""
1984 return self._com.SondeName
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
1991 @property
1992 def system_laser_power(self) -> float:
1993 """System laser power in milliwatts."""
1994 return float(self._com.SystemLaserPower)
1996 @property
1997 def unique_id(self) -> str:
1998 """Unique sensor-head identifier."""
1999 return self._com.UniqueID
2001 @property
2002 def upper_objective_threshold_temperature(self) -> float:
2003 """Upper objective-threshold temperature."""
2004 return float(self._com.UpperObjectiveThresholdTemperature)
2007class SensorHeadSettings:
2008 """Wrapper around COM sensor-head settings."""
2010 def __init__(self, obj: Any) -> None:
2011 self._com = obj
2013 @property
2014 def dimmer(self) -> float:
2015 """Dimmer value."""
2016 return float(self._com.Dimmer)
2018 @property
2019 def illumination_intensity(self) -> float:
2020 """Illumination intensity."""
2021 return float(self._com.IlluminationIntensity)
2023 @property
2024 def illumination_state(self) -> Any:
2025 """Illumination state."""
2026 return self._com.IlluminationState
2028 @property
2029 def key(self) -> Any:
2030 """Sensor-head identifier key."""
2031 return self._com.Key
2033 @property
2034 def name(self) -> str:
2035 """Sensor-head name."""
2036 return self._com.Name
2038 @property
2039 def sensor_head_info(self) -> SensorHeadInfo:
2040 """Information object for the sensor head."""
2041 return SensorHeadInfo(self._com.SensorHeadInfo)
2043 @property
2044 def video_camera_settings(self) -> VideoCameraSettings:
2045 """Video-camera settings for the sensor head.
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)
2057class FilterSettings:
2058 """Wrapper around COM controller-filter settings."""
2060 def __init__(self, obj: Any) -> None:
2061 self._com = obj
2063 @property
2064 def key(self) -> FilterType:
2065 """Filter type key."""
2066 return FilterType(self._com.Key)
2068 @property
2069 def name(self) -> str:
2070 """Filter name."""
2071 return self._com.Name
2073 @property
2074 def range(self) -> Any:
2075 """Filter range setting."""
2076 return self._com.Range
2079class DecoderInfo:
2080 """Wrapper around COM decoder information."""
2082 def __init__(self, obj: Any) -> None:
2083 self._com = obj
2085 @property
2086 def firmware_version(self) -> str:
2087 """Decoder firmware version."""
2088 return self._com.FirmwareVersion
2090 @property
2091 def name(self) -> str:
2092 """Decoder name."""
2093 return self._com.Name
2096class QuantitySettings:
2097 """Wrapper around COM quantity settings."""
2099 def __init__(self, obj: Any) -> None:
2100 self._com = obj
2102 @property
2103 def bandwidth_range(self) -> str:
2104 """Active decoder bandwidth range name."""
2105 return self._com.BandwidthRange
2107 @property
2108 def decoder_info(self) -> DecoderInfo:
2109 """Decoder information for this quantity."""
2110 return DecoderInfo(self._com.DecoderInfo)
2112 @property
2113 def key(self) -> QuantityType:
2114 """Quantity-type key."""
2115 return QuantityType(self._com.Key)
2117 @property
2118 def max_velocity_range(self) -> float:
2119 """Maximum velocity range."""
2120 return float(self._com.MaxVelocityRange)
2122 @property
2123 def name(self) -> str:
2124 """Quantity name."""
2125 return self._com.Name
2127 @property
2128 def output_active(self) -> bool:
2129 """Whether output of the quantity is active."""
2130 return bool(self._com.OutputActive)
2132 @property
2133 def overrun(self) -> Any:
2134 """Overrun mode/state."""
2135 return self._com.Overrun
2137 @property
2138 def range(self) -> str:
2139 """Active decoder range name."""
2140 return self._com.Range
2143class ControllerSettings:
2144 """Wrapper around COM vibrometer-controller settings."""
2146 def __init__(self, obj: Any) -> None:
2147 self._com = obj
2149 @property
2150 def controller_info(self) -> ControllerInfo:
2151 """Controller information."""
2152 return ControllerInfo(self._com.ControllerInfo)
2154 @property
2155 def decoder_clear_mode(self) -> Any:
2156 """Decoder clear mode."""
2157 return self._com.DecoderClearMode
2159 @property
2160 def filter_settings(self) -> list[FilterSettings]:
2161 """Filter settings."""
2162 return [FilterSettings(x) for x in self._com.FilterSettingsCollection]
2164 @property
2165 def quantity_settings(self) -> list[QuantitySettings]:
2166 """Quantity settings."""
2167 return [QuantitySettings(x) for x in self._com.QuantitySettingsCollection]
2169 @property
2170 def sensor_head_settings(self) -> list[SensorHeadSettings]:
2171 """Sensor-head settings."""
2172 return [SensorHeadSettings(x) for x in self._com.SensorHeadSettingsCollection]
2175class VibrometerProperties:
2176 """Wrapper around COM vibrometer acquisition properties."""
2178 def __init__(self, obj: Any) -> None:
2179 self._com = obj
2181 @property
2182 def controller_settings(self) -> ControllerSettings:
2183 """Vibrometer controller settings."""
2184 return ControllerSettings(self._com.VibControllerSettings)
2187class ZoomFFTProperties:
2188 """Wrapper around COM zoom-FFT acquisition properties."""
2190 def __init__(self, obj: Any) -> None:
2191 self._com = obj
2193 @property
2194 def bandwidth(self) -> float:
2195 """Zoom-FFT bandwidth in hertz."""
2196 return float(self._com.Bandwidth)
2198 @property
2199 def center_frequency(self) -> float:
2200 """Zoom-FFT center frequency in hertz."""
2201 return float(self._com.CenterFrequency)
2203 @property
2204 def lines(self) -> int:
2205 """Number of Zoom-FFT lines."""
2206 return int(self._com.Lines)
2208 @property
2209 def overlap(self) -> float:
2210 """Zoom-FFT overlap in percent."""
2211 return float(self._com.Overlap)
2213 @property
2214 def sample_frequency(self) -> float:
2215 """Sample frequency in hertz."""
2216 return float(self._com.SampleFrequency)
2218 @property
2219 def sample_resolution(self) -> float:
2220 """Frequency resolution in hertz."""
2221 return float(self._com.SampleResolution)
2223 @property
2224 def samples(self) -> int:
2225 """Number of samples used for the Zoom-FFT."""
2226 return int(self._com.Samples)
2228 @property
2229 def sample_time(self) -> float:
2230 """Sample time in seconds."""
2231 return float(self._com.SampleTime)
2234# Alignments
2235# Alignments
2238class AlignmentPoint2D:
2239 """Wrapper around a COM 2D alignment point."""
2241 def __init__(self, obj: Any) -> None:
2242 self._com = obj
2244 @property
2245 def scanner_x(self) -> float:
2246 """Horizontal scanner value of the 2D alignment point."""
2247 return float(self._com.ScannerX)
2249 @property
2250 def scanner_y(self) -> float:
2251 """Vertical scanner value of the 2D alignment point."""
2252 return float(self._com.ScannerY)
2254 @property
2255 def video_x(self) -> float:
2256 """X video coordinate of the 2D alignment point."""
2257 return float(self._com.VideoX)
2259 @property
2260 def video_y(self) -> float:
2261 """Y video coordinate of the 2D alignment point."""
2262 return float(self._com.VideoY)
2265class Alignment2D:
2266 """Wrapper around COM 2D alignment data and operations."""
2268 def __init__(self, obj: Any) -> None:
2269 self._com = obj
2271 @property
2272 def align_2d_points(self) -> list[AlignmentPoint2D]:
2273 """2D alignment points."""
2274 return [AlignmentPoint2D(p) for p in self._com.Align2DPoints]
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)
2281 @property
2282 def scan_head_type(self) -> ScanHeadType:
2283 """Type of scanning head used for the alignment."""
2284 return ScanHeadType(self._com.ScanHeadType)
2286 @property
2287 def scanner_quantity(self) -> Any:
2288 """Physical quantity represented by the scanner coordinates."""
2289 return self._com.ScannerQuantity
2291 @property
2292 def valid(self) -> bool:
2293 """Whether the 2D alignment is valid."""
2294 return bool(self._com.Valid)
2296 def calculate(self) -> None:
2297 """Calculate a new 2D alignment."""
2298 self._com.Calculate()
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 )
2312 def invalidate(self) -> None:
2313 """Invalidate the current 2D alignment and reset related properties."""
2314 self._com.Invalidate()
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)
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)
2325class AlignmentPoint3D:
2326 """Wrapper around a COM 3D alignment point."""
2328 def __init__(self, obj: Any) -> None:
2329 self._com = obj
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)
2336 @property
2337 def caps(self) -> AlignmentPoint3DCaps:
2338 """Capabilities of the 3D alignment point as an ``IntFlag``."""
2339 return AlignmentPoint3DCaps(self.caps_value)
2341 @property
2342 def distance(self) -> float:
2343 """Distance associated with the alignment point."""
2344 return float(self._com.Distance)
2346 @property
2347 def label(self) -> str:
2348 """Alignment-point label."""
2349 return self._com.Label
2351 @property
2352 def point_type(self) -> AlignmentPoint3DType:
2353 """Alignment-point type."""
2354 return AlignmentPoint3DType(self._com.PointType)
2356 @property
2357 def quality(self) -> float:
2358 """Alignment quality in meters."""
2359 return float(self._com.Quality)
2361 @property
2362 def scanner_x(self) -> float:
2363 """Horizontal scanner value of the 3D alignment point."""
2364 return float(self._com.ScannerX)
2366 @property
2367 def scanner_y(self) -> float:
2368 """Vertical scanner value of the 3D alignment point."""
2369 return float(self._com.ScannerY)
2371 @property
2372 def video_x(self) -> float:
2373 """X video coordinate of the 3D alignment point."""
2374 return float(self._com.VideoX)
2376 @property
2377 def video_y(self) -> float:
2378 """Y video coordinate of the 3D alignment point."""
2379 return float(self._com.VideoY)
2381 @property
2382 def x(self) -> float:
2383 """X coordinate of the 3D alignment point."""
2384 return float(self._com.X)
2386 @property
2387 def y(self) -> float:
2388 """Y coordinate of the 3D alignment point."""
2389 return float(self._com.Y)
2391 @property
2392 def z(self) -> float:
2393 """Z coordinate of the 3D alignment point."""
2394 return float(self._com.Z)
2397class Alignment3D:
2398 """Wrapper around COM 3D alignment data and operations."""
2400 def __init__(self, obj: Any) -> None:
2401 self._com = obj
2403 @property
2404 def align_3d_points(self) -> list[AlignmentPoint3D]:
2405 """3D alignment points."""
2406 return [AlignmentPoint3D(p) for p in self._com.Align3DPoints]
2408 @property
2409 def coord_definition_mode(self) -> CoordDefinitionMode:
2410 """Coordinate-definition mode for the 3D alignment."""
2411 return CoordDefinitionMode(self._com.CoordDefinitionMode)
2413 @property
2414 def current_quality(self) -> float:
2415 """Current 3D alignment quality in meters."""
2416 return float(self._com.CurrentQuality)
2418 @property
2419 def mirror(self) -> bool:
2420 """Whether the 3D alignment was performed through a mirror."""
2421 return bool(self._com.Mirror)
2423 @property
2424 def scan_head_type(self) -> ScanHeadType:
2425 """Scanning-head type."""
2426 return ScanHeadType(self._com.ScanHeadType)
2428 @property
2429 def target_quality(self) -> float:
2430 """Target 3D alignment quality in meters."""
2431 return float(self._com.TargetQuality)
2433 @property
2434 def valid(self) -> bool:
2435 """Whether the alignment is valid."""
2436 return bool(self._com.Valid)
2438 def calculate(self) -> None:
2439 """Calculate a new 3D alignment."""
2440 self._com.Calculate()
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)
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))
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)
2454 def get_scan_head_position(self) -> Any:
2455 """Retrieve the scan-head position at the first scanner mirror."""
2456 return self._com.GetScanHeadPosition()
2458 def invalidate(self) -> None:
2459 """Invalidate the current 3D alignment."""
2460 self._com.Invalidate()
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)
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)
2471class AlignmentCamera:
2472 """Wrapper around COM camera-alignment data and operations."""
2474 def __init__(self, obj: Any) -> None:
2475 self._com = obj
2477 @property
2478 def align_3d_points(self) -> list[AlignmentPoint3D]:
2479 """3D alignment points."""
2480 return [AlignmentPoint3D(p) for p in self._com.Align3DPoints]
2482 @property
2483 def aspect_ratio(self) -> float:
2484 """Video width-to-height aspect ratio."""
2485 return float(self._com.AspectRatio)
2487 @property
2488 def cam_pos_x(self) -> float:
2489 """X coordinate of the camera position."""
2490 return float(self._com.CamPosX)
2492 @property
2493 def cam_pos_y(self) -> float:
2494 """Y coordinate of the camera position."""
2495 return float(self._com.CamPosY)
2497 @property
2498 def cam_pos_z(self) -> float:
2499 """Z coordinate of the camera position."""
2500 return float(self._com.CamPosZ)
2502 @property
2503 def cam_vec_x(self) -> float:
2504 """X coordinate of the camera view direction."""
2505 return float(self._com.CamVecX)
2507 @property
2508 def cam_vec_y(self) -> float:
2509 """Y coordinate of the camera view direction."""
2510 return float(self._com.CamVecY)
2512 @property
2513 def cam_vec_z(self) -> float:
2514 """Z coordinate of the camera view direction."""
2515 return float(self._com.CamVecZ)
2517 @property
2518 def current_quality(self) -> float:
2519 """Current alignment quality."""
2520 return float(self._com.CurrentQuality)
2522 @property
2523 def mirror(self) -> bool:
2524 """Whether the object is viewed through a mirror."""
2525 return bool(self._com.Mirror)
2527 @property
2528 def target_quality(self) -> float:
2529 """Target alignment quality."""
2530 return float(self._com.TargetQuality)
2532 @property
2533 def valid(self) -> bool:
2534 """Whether the current alignment is valid."""
2535 return bool(self._com.Valid)
2537 @property
2538 def view_angle(self) -> float:
2539 """Camera aperture angle in degrees."""
2540 return float(self._com.ViewAngle)
2542 @property
2543 def view_up_x(self) -> float:
2544 """X coordinate of the camera view-up direction."""
2545 return float(self._com.ViewUpX)
2547 @property
2548 def view_up_y(self) -> float:
2549 """Y coordinate of the camera view-up direction."""
2550 return float(self._com.ViewUpY)
2552 @property
2553 def view_up_z(self) -> float:
2554 """Z coordinate of the camera view-up direction."""
2555 return float(self._com.ViewUpZ)
2557 def calculate(self, *args: Any) -> Any:
2558 """Calculate camera parameters from input parameters and alignment points."""
2559 return self._com.Calculate(*args)
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)
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)
2569 def scanner_to_video(self, *args: Any) -> Any:
2570 """Convert scanner coordinates to video coordinates."""
2571 return self._com.ScannerToVideo(*args)
2573 def set_camera(self, *args: Any) -> Any:
2574 """Set camera parameters."""
2575 return self._com.SetCamera(*args)
2577 def video_to_coord_3d(self, *args: Any) -> Any:
2578 """Convert video coordinates to 3D object coordinates."""
2579 return self._com.VideoToCoord3D(*args)
2581 def video_to_scanner(self, *args: Any) -> Any:
2582 """Convert video coordinates to scanner coordinates."""
2583 return self._com.VideoToScanner(*args)
2586class Alignments:
2587 """Wrapper around the COM alignments info object."""
2589 def __init__(self, obj: Any) -> None:
2590 self._com = obj
2592 @property
2593 def alignments_2d(self) -> list[Alignment2D]:
2594 """2D alignments."""
2595 return [Alignment2D(a) for a in self._com.Alignments2D]
2597 @property
2598 def alignments_3d(self) -> list[Alignment3D]:
2599 """3D alignments."""
2600 return [Alignment3D(a) for a in self._com.Alignments3D]
2602 @property
2603 def alignments_camera(self) -> list[AlignmentCamera]:
2604 """Camera alignments."""
2605 return [AlignmentCamera(a) for a in self._com.AlignmentsCamera]
2607 @property
2608 def name(self) -> str:
2609 """Info-object name."""
2610 return self._com.Name
2612 @property
2613 def type(self) -> Any:
2614 """Info-object type identifier."""
2615 return self._com.Type
2617 def get_video_rect(self) -> tuple[float, float, float, float]:
2618 """Gets the video rectangle defining the video coordinate system.
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()
2634class Element:
2635 """Wrapper around a COM geometry element."""
2637 def __init__(self, obj: Any) -> None:
2638 self._com = obj
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]
2645 @property
2646 def meas_point_labels(self) -> list[Any]:
2647 """Measurement-point labels used by the element."""
2648 return list(self._com.MeasPointLabels)
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 ]
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 )
2676class FrontEnd:
2677 """Wrapper around COM front-end information."""
2679 def __init__(self, obj: Any, *, expose_internals: bool = True) -> None:
2680 self._com = obj
2681 self._expose_internals = expose_internals
2683 @property
2684 def caps_value(self) -> int:
2685 """Front-end capabilities as a raw bitmask."""
2686 return int(self._com.Caps)
2688 @property
2689 def caps(self) -> FrontEndCaps:
2690 """Front-end capabilities as a decoded flag set."""
2691 return FrontEndCaps(self.caps_value)
2693 @property
2694 def firmware_version(self) -> str:
2695 """Firmware version of the front-end."""
2696 return self._com.FirmwareVersion
2698 @property
2699 def host_ip_address(self) -> str:
2700 """IP address of the connected host network adapter."""
2701 return self._com.HostIpAddress
2703 @property
2704 def mac_address(self) -> str:
2705 """MAC address of the front-end."""
2706 return self._com.MacAddress
2708 @property
2709 def name(self) -> str:
2710 """Front-end name."""
2711 return self._com.Name
2713 @property
2714 def type(self) -> FrontEndType:
2715 """Front-end type."""
2716 return FrontEndType(self._com.Type)
2718 @property
2719 def front_ends(self) -> Any:
2720 """Parent raw COM front-end collection."""
2721 return self._com.FrontEnds
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
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
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
2753class Hardware:
2754 """Wrapper around COM hardware info."""
2756 def __init__(self, obj: Any) -> None:
2757 self._com = obj
2759 @property
2760 def acq_board(self) -> AcqBoard:
2761 """Acquisition-board type used for the measurement."""
2762 return AcqBoard(self._com.AcqBoard)
2764 @property
2765 def acq_board_channel_count(self) -> int:
2766 """Number of acquisition-board channels."""
2767 return int(self._com.AcqBoardChannelCount)
2769 @property
2770 def acq_board_firmware_version(self) -> str:
2771 """Acquisition-board firmware version."""
2772 return self._com.AcqBoardFirmwareVersion
2774 @property
2775 def acq_board_max_frequency(self) -> float:
2776 """Maximum acquisition-board frequency."""
2777 return float(self._com.AcqBoardMaxFrequency)
2779 @property
2780 def acq_board_name(self) -> str:
2781 """Acquisition-board name."""
2782 return self._com.AcqBoardName
2784 @property
2785 def active_front_end(self) -> FrontEnd:
2786 """Active front-end used for the measurement."""
2787 return FrontEnd(self._com.ActiveFrontEnd)
2789 @property
2790 def coherence_optimizer_versions(self) -> list[str]:
2791 """Versions of system coherence optimizers."""
2792 return list(self._com.CoherenceOptimizerVersions)
2794 @property
2795 def front_end(self) -> FrontEndType:
2796 """Front-end type used for the measurement."""
2797 return FrontEndType(self._com.FrontEnd)
2799 @property
2800 def front_end_firmware_version(self) -> str:
2801 """Front-end firmware version."""
2802 return self._com.FrontEndFirmwareVersion
2804 @property
2805 def generator_channel_count(self) -> int:
2806 """Number of generator channels."""
2807 return int(self._com.GeneratorChannelCount)
2809 @property
2810 def generator_firmware_version(self) -> str:
2811 """Generator firmware version."""
2812 return self._com.GeneratorFirmwareVersion
2814 @property
2815 def generator_name(self) -> str:
2816 """Generator name."""
2817 return self._com.GeneratorName
2819 @property
2820 def generator_type(self) -> GeneratorType:
2821 """Generator type."""
2822 return GeneratorType(self._com.GeneratorType)
2824 @property
2825 def name(self) -> str:
2826 """Info-object name."""
2827 return self._com.Name
2829 @property
2830 def scan_head_type(self) -> ScanHeadType:
2831 """Scanning-head type used for the measurement."""
2832 return ScanHeadType(self._com.ScanHeadType)
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)
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)
2844 @property
2845 def teds_sensors(self) -> Any:
2846 """TEDS sensor information."""
2847 return self._com.TedsSensors
2849 @property
2850 def type(self) -> Any:
2851 """Info-object type identifier."""
2852 return self._com.Type
2855class GeometryComponent:
2856 """Wrapper around a COM geometry component."""
2858 def __init__(self, obj: Any) -> None:
2859 self._com = obj
2861 @property
2862 def components(self) -> list["GeometryComponent"]:
2863 """Child geometry components."""
2864 return [GeometryComponent(c) for c in self._com.Components]
2866 @property
2867 def description(self) -> str:
2868 """Geometry-component description."""
2869 return self._com.Description
2871 @property
2872 def id(self) -> Any:
2873 """Unique identifier of the geometry component."""
2874 return self._com.ID
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]
2881 @property
2882 def labels(self) -> list[Any]:
2883 """Measurement-point labels attached to the component."""
2884 return list(self._com.Labels)
2886 @property
2887 def name(self) -> str:
2888 """Geometry-component name."""
2889 return self._com.Name
2892class MeasPoint:
2893 """Wrapper around a COM measurement point."""
2895 def __init__(self, obj: Any) -> None:
2896 self._com = obj
2898 def coord_xyz(self) -> Any:
2899 """Retrieve the 3D coordinate of the measurement point."""
2900 return self._com.CoordXYZ(0, 0, 0)
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)
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)
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)
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)
2918 def toggle_valid_invalidated(self) -> None:
2919 """Toggle the invalidated state of the measurement point."""
2920 self._com.ToggleValidInvalidated()
2922 def video_xy(self) -> Any:
2923 """Retrieve the video coordinate of the measurement point."""
2924 return self._com.VideoXY(0, 0)
2926 @property
2927 def average_count(self) -> int:
2928 """Average count for the measurement point."""
2929 return int(self._com.AverageCount)
2931 @property
2932 def component(self) -> GeometryComponent:
2933 """Geometry component to which this point belongs."""
2934 return GeometryComponent(self._com.Component)
2936 @property
2937 def contained_file_active_id(self) -> Any:
2938 """Contained-file active identifier for combined files."""
2939 return self._com.ContainedFileActiveID
2941 @property
2942 def contained_file_active_mp_index(self) -> int:
2943 """Active contained-file measurement-point index."""
2944 return int(self._com.ContainedFileActiveMPIndex)
2946 @property
2947 def contained_file_index(self) -> int:
2948 """Contained-file index for combined files."""
2949 return int(self._com.ContainedFileIndex)
2951 @property
2952 def contained_file_meas_point_index(self) -> int:
2953 """Contained-file measurement-point index."""
2954 return int(self._com.ContainedFileMeasPointIndex)
2956 @property
2957 def focus_status(self) -> FocusStatus:
2958 """Laser-focus status flags."""
2959 return FocusStatus(int(self._com.FocusStatus))
2961 @property
2962 def focus_values(self) -> Any:
2963 """Laser-focus values."""
2964 return self._com.FocusValues
2966 @property
2967 def geometry_status(self) -> GeometryStatus:
2968 """3D geometry status flags."""
2969 return GeometryStatus(int(self._com.GeometryStatus))
2971 @property
2972 def index(self) -> int:
2973 """Measurement-point index."""
2974 return int(self._com.Index)
2976 @property
2977 def label(self) -> Any:
2978 """Measurement-point label."""
2979 return self._com.Label
2981 @property
2982 def min_sigma(self) -> float:
2983 """Minimum signal-enhancement sigma."""
2984 return float(self._com.MinSigma)
2986 @property
2987 def path_length_status(self) -> Any:
2988 """Optical path-length status."""
2989 return self._com.PathLengthStatus
2991 @property
2992 def path_length_value(self) -> Any:
2993 """Optical path-length value."""
2994 return self._com.PathLengthValue
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]
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]
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]
3011 @property
3012 def scan_quantity(self) -> PhysicalQuantity:
3013 """Physical quantity of the scanner values."""
3014 return PhysicalQuantity(self._com.ScanQuantity)
3016 @property
3017 def scan_status(self) -> ScanStatus:
3018 """Measurement status flags."""
3019 return ScanStatus(int(self._com.ScanStatus))
3021 @property
3022 def scan_status_disabled(self) -> bool:
3023 """Whether the scan-status disabled flag is set."""
3024 return bool(self._com.ScanStatusDisabled)
3026 @property
3027 def sigma(self) -> float:
3028 """Signal-enhancement sigma."""
3029 return float(self._com.Sigma)
3031 @property
3032 def vibrometer_range(self) -> Any:
3033 """Vibrometer range used for the point."""
3034 return self._com.VibrometerRange
3037class MeasPoints:
3038 """Pythonic wrapper around the COM measurement-point collection."""
3040 def __init__(self, obj: Any) -> None:
3041 self._com = obj
3043 def __len__(self) -> int:
3044 """Number of measurement points."""
3045 return int(self._com.Count)
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))
3056 def __iter__(self):
3057 """Iterate over measurement points."""
3058 for i in range(len(self)):
3059 yield self[i]
3061 @property
3062 def average_sigma(self) -> float:
3063 """Average signal-enhancement sigma for all points."""
3064 return float(self._com.AverageSigma)
3066 @property
3067 def component(self) -> GeometryComponent:
3068 """Root geometry component."""
3069 return GeometryComponent(self._com.Component)
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))
3076 @property
3077 def count(self) -> int:
3078 """Number of measurement points."""
3079 return int(self._com.Count)
3081 @property
3082 def has_3d_coordinates(self) -> bool:
3083 """Whether the points are defined with 3D coordinates."""
3084 return bool(self._com.Has3DCoordinates)
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)
3091 @property
3092 def has_sigma(self) -> bool:
3093 """Whether average signal-enhancement sigma is valid."""
3094 return bool(self._com.HasSigma)
3096 @property
3097 def is_3d_calculation_possible(self) -> bool:
3098 """Whether 3D coordinate calculation is possible."""
3099 return bool(self._com.Is3DCalculationPossible)
3101 @property
3102 def name(self) -> str:
3103 """Info-object name."""
3104 return self._com.Name
3106 @property
3107 def type(self) -> Any:
3108 """Info-object type identifier."""
3109 return self._com.Type
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))
3116 def get_video_rect(self) -> tuple[float, float, float, float]:
3117 """Gets the video rectangle defining the video coordinate system.
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()
3133class MeasurementLocation:
3134 """Wrapper around a COM measurement-location object."""
3136 def __init__(self, obj: Any) -> None:
3137 self._com = obj
3139 @property
3140 def channel_name(self) -> str:
3141 """Channel name."""
3142 return self._com.ChannelName
3144 @property
3145 def direction(self) -> Any:
3146 """Measurement direction or directions in object coordinates."""
3147 return self._com.Direction
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)
3154 @property
3155 def mode(self) -> MeasurementLocationMode:
3156 """Measurement-location mode."""
3157 return MeasurementLocationMode(self._com.Mode)
3159 @property
3160 def normal(self) -> Any:
3161 """Normal vector or vectors in object coordinates."""
3162 return self._com.Normal
3164 @property
3165 def status(self) -> ScanStatus:
3166 """Measurement status of the referenced point."""
3167 return ScanStatus(int(self._com.Status))
3170class Profile:
3171 """Wrapper around a COM profile object."""
3173 def __init__(self, obj: Any) -> None:
3174 self._com = obj
3176 @property
3177 def color(self) -> Any:
3178 """Profile color code."""
3179 return self._com.Color
3181 @property
3182 def lines(self) -> Any:
3183 """Profile line coordinates in video units."""
3184 return self._com.Lines
3186 @property
3187 def type(self) -> ProfileType:
3188 """Profile type."""
3189 return ProfileType(self._com.Type)
3192class DistanceSensorControl:
3193 """Wrapper around COM distance-sensor control."""
3195 def __init__(self, obj: Any) -> None:
3196 self._com = obj
3198 @property
3199 def connected(self) -> bool:
3200 """Whether the geometry scan unit is connected."""
3201 return bool(self._com.Connected)
3203 @property
3204 def filter_on(self) -> bool:
3205 """Whether the geometry scan-unit filter is enabled."""
3206 return bool(self._com.FilterOn)
3208 @property
3209 def laser_on(self) -> bool:
3210 """Whether the geometry laser is on."""
3211 return bool(self._com.LaserOn)
3213 @property
3214 def scan_head_device(self) -> "ScanHeadDevice":
3215 """Associated scan-head device."""
3216 return ScanHeadDevice(self._com.ScanHeadDevice)
3218 @property
3219 def type(self) -> DistanceSensorType:
3220 """Distance-sensor type."""
3221 return DistanceSensorType(self._com.Type)
3224class ScanHeadLaserFocus:
3225 """Wrapper around COM scan-head focus settings."""
3227 def __init__(self, obj: Any) -> None:
3228 self._com = obj
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)
3234 @property
3235 def depth_of_focus_factor(self) -> float:
3236 """Depth-of-focus factor."""
3237 return float(self._com.DepthOfFocusFactor)
3239 @property
3240 def focal_length(self) -> float:
3241 """Objective focal length."""
3242 return float(self._com.FocalLength)
3244 @property
3245 def focus_command_time(self) -> float:
3246 """Time per focus command in seconds."""
3247 return float(self._com.FocusCommandTime)
3249 @property
3250 def focus_sharpness(self) -> float:
3251 """Focus sharpness."""
3252 return float(self._com.FocusSharpness)
3254 @property
3255 def focus_step_width(self) -> float:
3256 """Focus step width in meters."""
3257 return float(self._com.FocusStepWidth)
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)
3264 @property
3265 def min_focus_distance(self) -> float:
3266 """Minimum focus distance in meters."""
3267 return float(self._com.MinFocusDistance)
3269 @property
3270 def scan_head_device(self) -> "ScanHeadDevice":
3271 """Associated scan-head device."""
3272 return ScanHeadDevice(self._com.ScanHeadDevice)
3274 @property
3275 def time_per_focus_step(self) -> float:
3276 """Time per focus step in seconds."""
3277 return float(self._com.TimePerFocusStep)
3280class ScanHeadScanner:
3281 """Wrapper around COM scan-head scanner settings."""
3283 def __init__(self, obj: Any) -> None:
3284 self._com = obj
3286 def get_laser_origin(self) -> Any:
3287 """Return the laser origin at 0°/0° beam direction."""
3288 return self._com.GetLaserOrigin()
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)
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)
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)
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)
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)
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)
3318 @property
3319 def inverted_x(self) -> bool:
3320 """Whether horizontal scanner polarity is inverted."""
3321 return bool(self._com.InvertedX)
3323 @property
3324 def inverted_y(self) -> bool:
3325 """Whether vertical scanner polarity is inverted."""
3326 return bool(self._com.InvertedY)
3328 @property
3329 def mirror_distance(self) -> float:
3330 """Distance between the two scanner mirrors in meters."""
3331 return float(self._com.MirrorDistance)
3333 @property
3334 def preferred_scan_direction(self) -> PreferredScanDirection:
3335 """Preferred scan direction."""
3336 return PreferredScanDirection(self._com.PreferredScanDirection)
3338 @property
3339 def quantity(self) -> PhysicalQuantity:
3340 """Physical quantity of the scanning head."""
3341 return PhysicalQuantity(self._com.Quantity)
3343 @property
3344 def quantity_max_x(self) -> float:
3345 """Maximum horizontal scanner value."""
3346 return float(self._com.QuantityMaxX)
3348 @property
3349 def quantity_max_y(self) -> float:
3350 """Maximum vertical scanner value."""
3351 return float(self._com.QuantityMaxY)
3353 @property
3354 def scan_head_device(self) -> "ScanHeadDevice":
3355 """Associated scan-head device."""
3356 return ScanHeadDevice(self._com.ScanHeadDevice)
3358 @property
3359 def swapped_xy(self) -> bool:
3360 """Whether horizontal and vertical scanners are swapped."""
3361 return bool(self._com.SwappedXY)
3364class ScanHeadVideo:
3365 """Wrapper around COM scan-head video settings."""
3367 def __init__(self, obj: Any) -> None:
3368 self._com = obj
3370 @property
3371 def flipped_x(self) -> bool:
3372 """Whether the video image is flipped horizontally."""
3373 return bool(self._com.FlippedX)
3375 @property
3376 def flipped_y(self) -> bool:
3377 """Whether the video image is flipped vertically."""
3378 return bool(self._com.FlippedY)
3380 @property
3381 def scan_head_device(self) -> "ScanHeadDevice":
3382 """Associated scan-head device."""
3383 return ScanHeadDevice(self._com.ScanHeadDevice)
3385 @property
3386 def type_value(self) -> int:
3387 """Video type as a raw bitmask."""
3388 return int(self._com.Type)
3390 @property
3391 def type(self) -> ScanHeadVideoType:
3392 """Video type/capability flags."""
3393 return ScanHeadVideoType(self.type_value)
3396class ScanHeadDevice:
3397 """Wrapper around a COM scan-head device."""
3399 def __init__(self, obj: Any) -> None:
3400 self._com = obj
3402 @property
3403 def caps_value(self) -> int:
3404 """Scanning-head capabilities as a raw bitmask."""
3405 return int(self._com.Caps)
3407 @property
3408 def caps(self) -> ScanHeadCaps:
3409 """Scanning-head capabilities as an ``IntFlag``."""
3410 return ScanHeadCaps(self.caps_value)
3412 @property
3413 def distance_sensor_control(self) -> DistanceSensorControl:
3414 """Distance-sensor control object."""
3415 return DistanceSensorControl(self._com.DistanceSensorControl)
3417 @property
3418 def focus(self) -> ScanHeadLaserFocus:
3419 """Scanning-head focus object."""
3420 return ScanHeadLaserFocus(self._com.Focus)
3422 @property
3423 def lens_magnification(self) -> float:
3424 """Objective magnification."""
3425 return float(self._com.LensMagnification)
3427 @property
3428 def pan_tilt_head(self) -> bool:
3429 """Whether a pan-tilt head is used."""
3430 return bool(self._com.PanTiltHead)
3432 @property
3433 def roll_angle(self) -> float:
3434 """Roll angle of the scanning head."""
3435 return float(self._com.RollAngle)
3437 @property
3438 def scanner(self) -> ScanHeadScanner:
3439 """Scanning-head scanner object."""
3440 return ScanHeadScanner(self._com.Scanner)
3442 @property
3443 def sensor_head_name(self) -> str:
3444 """Name of the active sensor head."""
3445 return self._com.SensorHeadName
3447 @property
3448 def sensor_head_names(self) -> list[str]:
3449 """All possible sensor-head names."""
3450 return list(self._com.SensorHeadNames)
3452 @property
3453 def video(self) -> ScanHeadVideo:
3454 """Scanning-head video object."""
3455 return ScanHeadVideo(self._com.Video)
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]
3463class ScanHeadDevicesInfo:
3464 """Wrapper around COM scan-head-devices info."""
3466 def __init__(self, obj: Any) -> None:
3467 self._com = obj
3469 @property
3470 def name(self) -> str:
3471 """Info-object name."""
3472 return self._com.Name
3474 @property
3475 def scan_head_devices(self) -> list[ScanHeadDevice]:
3476 """Scan-head devices."""
3477 return [ScanHeadDevice(v) for v in self._com.ScanHeadDevices]
3479 @property
3480 def scan_head_name(self) -> str:
3481 """Scanning-head name."""
3482 return self._com.ScanHeadName
3484 @property
3485 def scan_head_type(self) -> ScanHeadType:
3486 """Scanning-head type."""
3487 return ScanHeadType(self._com.ScanHeadType)
3489 @property
3490 def type(self) -> Any:
3491 """Info-object type identifier."""
3492 return self._com.Type
3495class SpectrogramInfo:
3496 """Wrapper around COM spectrogram/campbell-diagram info."""
3498 def __init__(self, obj: Any) -> None:
3499 self._com = obj
3501 @property
3502 def bandwidth(self) -> float:
3503 """Bandwidth used for spectrogram FFT calculation."""
3504 return float(self._com.Bandwidth)
3506 @property
3507 def bandwidth_from(self) -> float:
3508 """Lower bandwidth limit."""
3509 return float(self._com.BandwidthFrom)
3511 @property
3512 def bandwidth_to(self) -> float:
3513 """Upper bandwidth limit."""
3514 return float(self._com.BandwidthTo)
3516 @property
3517 def effective_sample_count(self) -> int:
3518 """Effective sample count used for FFT calculation."""
3519 return int(self._com.EffectiveSampleCount)
3521 @property
3522 def fft_count(self) -> int:
3523 """Number of FFTs in the resulting spectrogram."""
3524 return int(self._com.FFTCount)
3526 @property
3527 def fft_lines(self) -> int:
3528 """Number of FFT lines."""
3529 return int(self._com.FFTLines)
3531 @property
3532 def frequency_resolution(self) -> float:
3533 """Frequency resolution in hertz."""
3534 return float(self._com.FrequencyResolution)
3536 @property
3537 def name(self) -> str:
3538 """Info-object name."""
3539 return self._com.Name
3541 @property
3542 def overlap(self) -> float:
3543 """FFT overlap in percent."""
3544 return float(self._com.Overlap)
3546 @property
3547 def rpm_channel(self) -> str:
3548 """Name of the RPM channel."""
3549 return self._com.RPMChannel
3551 @property
3552 def sample_count(self) -> int:
3553 """Number of samples used to calculate an FFT."""
3554 return int(self._com.SampleCount)
3556 @property
3557 def sample_rate(self) -> float:
3558 """Time-trace sample rate in hertz."""
3559 return float(self._com.SampleRate)
3561 @property
3562 def sample_rate_bandwidth_ratio(self) -> float:
3563 """Ratio between sample rate and bandwidth."""
3564 return float(self._com.SampleRateBandwidthRatio)
3566 @property
3567 def time_from(self) -> float:
3568 """Start of the time range considered."""
3569 return float(self._com.TimeFrom)
3571 @property
3572 def time_to(self) -> float:
3573 """End of the time range considered."""
3574 return float(self._com.TimeTo)
3576 @property
3577 def total_sample_count(self) -> int:
3578 """Total number of samples available."""
3579 return int(self._com.TotalSampleCount)
3581 @property
3582 def type(self) -> Any:
3583 """Spectrogram type identifier."""
3584 return self._com.Type
3586 @property
3587 def used_fft_lines(self) -> int:
3588 """Number of used FFT lines."""
3589 return int(self._com.UsedFFTLines)
3591 @property
3592 def window_function(self) -> WindowFunction:
3593 """Window function used before FFT calculation."""
3594 return WindowFunction(self._com.WindowFunction)
3596 @property
3597 def window_parameter(self) -> Any:
3598 """Window-function parameter(s)."""
3599 return self._com.WindowParameter
3602class Texture:
3603 """Wrapper around a COM texture object."""
3605 def __init__(self, obj: Any) -> None:
3606 self._com = obj
3608 @property
3609 def height(self) -> int:
3610 """Texture height in pixels."""
3611 return int(self._com.Height)
3613 @property
3614 def image(self) -> bytes:
3615 """Texture image bytes in BMP file format."""
3616 return bytes(self._com.Image)
3618 @property
3619 def width(self) -> int:
3620 """Texture width in pixels."""
3621 return int(self._com.Width)
3624class VideoBitmap:
3625 """Wrapper around COM video-bitmap info."""
3627 def __init__(self, obj: Any) -> None:
3628 self._com = obj
3630 def get_3d_video_offset(self) -> Any:
3631 """Get the video-position offset in 3D coordinates."""
3632 return self._com.Get3DVideoOffset()
3634 def get_image_rect(self) -> Any:
3635 """Get the image rectangle in video coordinates."""
3636 return self._com.GetImageRect()
3638 def get_video_offset(self) -> Any:
3639 """Get the video-position offset in video coordinates."""
3640 return self._com.GetVideoOffset()
3642 def load_image_file(self, file_name: str) -> None:
3643 """Load an image from the given file."""
3644 self._com.LoadImageFile(file_name)
3646 def save(self) -> None:
3647 """Commit changes to the underlying file."""
3648 self._com.Save()
3650 def set_image(self, image_bytes: bytes) -> None:
3651 """Set the image from an array of bytes."""
3652 self._com.SetImage(image_bytes)
3654 def image_bytes(
3655 self, fmt: GraphicFormatType = GraphicFormatType.BITMAP
3656 ) -> tuple[bytes, int, int]:
3657 """Return image bytes and dimensions.
3659 Parameters
3660 ----------
3661 fmt
3662 Graphic format to request from the COM interface.
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)
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]))
3676 @property
3677 def name(self) -> str:
3678 """Info-object name."""
3679 return self._com.Name
3681 @property
3682 def texture_columns(self) -> int:
3683 """Number of columns in a stitched combined-file image."""
3684 return int(self._com.TextureColumns)
3686 @property
3687 def texture_coordinates(self) -> Any:
3688 """Texture coordinates for scan points."""
3689 return self._com.TextureCoordinates
3691 @property
3692 def texture_count(self) -> int:
3693 """Number of stitched video bitmaps."""
3694 return int(self._com.TextureCount)
3696 @property
3697 def texture_rows(self) -> int:
3698 """Number of rows in a stitched combined-file image."""
3699 return int(self._com.TextureRows)
3701 @property
3702 def type(self) -> Any:
3703 """Info-object type identifier."""
3704 return self._com.Type
3707class LensCalibration:
3708 """Wrapper around COM lens calibration information."""
3710 def __init__(self, obj: Any) -> None:
3711 self._com = obj
3713 @property
3714 def caps_value(self) -> int:
3715 """Lens-calibration capabilities as a raw bitmask."""
3716 return int(self._com.Caps)
3718 @property
3719 def caps(self) -> LensCalibrationCaps:
3720 """Lens-calibration capabilities as an ``IntFlag``."""
3721 return LensCalibrationCaps(self.caps_value)
3723 @property
3724 def distance_x(self) -> float:
3725 """Horizontal calibration distance in meters."""
3726 return float(self._com.DistanceX)
3728 @property
3729 def distance_y(self) -> float:
3730 """Vertical calibration distance in meters."""
3731 return float(self._com.DistanceY)
3733 @property
3734 def magnification_factor_x(self) -> float:
3735 """Horizontal magnification factor."""
3736 return float(self._com.MagnificationFactorX)
3738 @property
3739 def magnification_factor_y(self) -> float:
3740 """Vertical magnification factor."""
3741 return float(self._com.MagnificationFactorY)
3743 @property
3744 def name(self) -> str:
3745 """Calibration name."""
3746 return self._com.Name
3748 @property
3749 def pixels_x(self) -> int:
3750 """Horizontal calibration distance in pixels."""
3751 return int(self._com.PixelsX)
3753 @property
3754 def pixels_y(self) -> int:
3755 """Vertical calibration distance in pixels."""
3756 return int(self._com.PixelsY)
3759class CameraSize:
3760 """Wrapper around COM camera-size information."""
3762 def __init__(self, obj: Any) -> None:
3763 self._com = obj
3765 @property
3766 def chip_x(self) -> float:
3767 """Horizontal camera-chip size in meters."""
3768 return float(self._com.ChipX)
3770 @property
3771 def chip_y(self) -> float:
3772 """Vertical camera-chip size in meters."""
3773 return float(self._com.ChipY)
3775 @property
3776 def image_x(self) -> int:
3777 """Horizontal pixel count of the camera chip."""
3778 return int(self._com.ImageX)
3780 @property
3781 def image_y(self) -> int:
3782 """Vertical pixel count of the camera chip."""
3783 return int(self._com.ImageY)
3786class VideoMappingInfo:
3787 """Wrapper around COM video-mapping information."""
3789 def __init__(self, obj: Any) -> None:
3790 self._com = obj
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)
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)
3802 def save(self) -> None:
3803 """Save modifications to the underlying file."""
3804 self._com.Save()
3806 @property
3807 def active_calibration(self) -> str:
3808 """Name of the active objective calibration."""
3809 return self._com.ActiveCalibration
3811 @property
3812 def calibrations(self) -> list[LensCalibration]:
3813 """Available lens calibrations."""
3814 return [LensCalibration(c) for c in self._com.Calibrations]
3816 @property
3817 def camera_size(self) -> CameraSize:
3818 """Camera-size object."""
3819 return CameraSize(self._com.CameraSize)
3821 @property
3822 def name(self) -> str:
3823 """Info-object name."""
3824 return self._com.Name
3826 @property
3827 def type(self) -> Any:
3828 """Info-object type identifier."""
3829 return self._com.Type
3832class XAxis:
3833 """Wrapper around a COM x-axis descriptor."""
3835 def __init__(self, obj: Any) -> None:
3836 self._com = obj
3838 @property
3839 def max(self) -> float:
3840 """Maximum x-axis value."""
3841 return float(self._com.Max)
3843 @property
3844 def max_count(self) -> int:
3845 """Number of x-axis samples."""
3846 return int(self._com.MaxCount)
3848 @property
3849 def min(self) -> float:
3850 """Minimum x-axis value."""
3851 return float(self._com.Min)
3853 @property
3854 def name(self) -> str:
3855 """X-axis label."""
3856 return self._com.Name
3858 @property
3859 def unit(self) -> str:
3860 """X-axis unit."""
3861 return self._com.Unit
3864class YAxis:
3865 """Wrapper around a COM y-axis descriptor."""
3867 def __init__(self, obj: Any) -> None:
3868 self._com = obj
3870 @property
3871 def max(self) -> float:
3872 """Maximum y-axis value."""
3873 return float(self._com.Max)
3875 @property
3876 def min(self) -> float:
3877 """Minimum y-axis value."""
3878 return float(self._com.Min)
3880 @property
3881 def name(self) -> str:
3882 """Y-axis label."""
3883 return self._com.Name
3885 @property
3886 def unit(self) -> str:
3887 """Y-axis unit."""
3888 return self._com.Unit
3891class Domain:
3892 """Wrapper around a COM domain."""
3894 def __init__(self, obj: Any) -> None:
3895 self._com = obj
3897 @property
3898 def channels(self) -> list["Channel"]:
3899 """Channels in the domain."""
3900 return [Channel(c) for c in self._com.Channels]
3902 @property
3903 def name(self) -> str:
3904 """Domain name."""
3905 return self._com.Name
3907 @property
3908 def type(self) -> DomainType:
3909 """Domain type."""
3910 return DomainType(self._com.Type)
3913class Channel:
3914 """Wrapper around a COM signal channel."""
3916 def __init__(self, obj: Any) -> None:
3917 self._com = obj
3919 @property
3920 def caps_value(self) -> int:
3921 """Channel capabilities as a raw bitmask."""
3922 return int(self._com.Caps)
3924 @property
3925 def caps(self) -> ChannelCaps:
3926 """Channel capabilities as an ``IntFlag``."""
3927 return ChannelCaps(self.caps_value)
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)
3934 @property
3935 def domain(self) -> Domain:
3936 """Parent domain."""
3937 return Domain(self._com.Domain)
3939 @property
3940 def name(self) -> str:
3941 """Channel name."""
3942 return self._com.Name
3944 @property
3945 def signals(self) -> list["Signal"]:
3946 """Signals available on the channel."""
3947 return [Signal(s) for s in self._com.Signals]
3950class Signal:
3951 """Wrapper around a COM signal."""
3953 def __init__(self, obj: Any) -> None:
3954 self._com = obj
3956 @property
3957 def channel(self) -> Channel:
3958 """Parent channel."""
3959 return Channel(self._com.Channel)
3961 @property
3962 def description(self) -> "SignalDescription":
3963 """Signal description."""
3964 return SignalDescription(self._com.Description)
3966 @property
3967 def displays(self) -> list["Display"]:
3968 """Available displays for the signal."""
3969 return [Display(d) for d in self._com.Displays]
3971 @property
3972 def name(self) -> str:
3973 """Signal name."""
3974 return self._com.Name
3977class Display:
3978 """Wrapper around a COM signal display."""
3980 def __init__(self, obj: Any) -> None:
3981 self._com = obj
3983 @property
3984 def name(self) -> str:
3985 """Display name."""
3986 return self._com.Name
3988 @property
3989 def signal(self) -> Signal:
3990 """Parent signal."""
3991 return Signal(self._com.Signal)
3993 @property
3994 def type(self) -> DisplayType:
3995 """Display type."""
3996 return DisplayType(self._com.Type)
3999class Attribute:
4000 """Wrapper around a COM signal attribute."""
4002 def __init__(self, obj: Any) -> None:
4003 self._com = obj
4005 @property
4006 def description(self) -> str:
4007 """Attribute description."""
4008 return self._com.Description
4010 @property
4011 def name(self) -> str:
4012 """Attribute name."""
4013 return self._com.Name
4015 @property
4016 def quantity(self) -> Any:
4017 """Physical quantity of the attribute value."""
4018 return self._com.Quantity
4020 @property
4021 def unit(self) -> str:
4022 """Unit of the attribute value."""
4023 return self._com.Unit
4025 @property
4026 def value(self) -> Any:
4027 """Attribute value."""
4028 return self._com.Value
4031class DegreeOfFreedomID:
4032 """Wrapper around a COM degree-of-freedom identifier."""
4034 def __init__(self, obj: Any) -> None:
4035 self._com = obj
4037 @property
4038 def channel_name(self) -> str:
4039 """Channel name for the DOF."""
4040 return self._com.ChannelName
4042 @property
4043 def direction(self) -> DOFDirection:
4044 """DOF direction."""
4045 return DOFDirection(self._com.Direction)
4047 @property
4048 def node(self) -> Any:
4049 """DOF node number or identifier."""
4050 return self._com.Node
4052 @property
4053 def node_description(self) -> str:
4054 """Node description."""
4055 return self._com.NodeDescription
4057 @property
4058 def quantity(self) -> Any:
4059 """Physical quantity of the DOF channel."""
4060 return self._com.Quantity
4062 @property
4063 def unit(self) -> str:
4064 """Unit of the DOF channel."""
4065 return self._com.Unit
4068class SignalXAxis:
4069 """Wrapper around a COM signal x-axis descriptor."""
4071 def __init__(self, obj: Any) -> None:
4072 self._com = obj
4074 @property
4075 def max(self) -> float:
4076 """Maximum x-axis value."""
4077 return float(self._com.Max)
4079 @property
4080 def max_count(self) -> int:
4081 """Number of x-axis samples."""
4082 return int(self._com.MaxCount)
4084 @property
4085 def min(self) -> float:
4086 """Minimum x-axis value."""
4087 return float(self._com.Min)
4089 @property
4090 def name(self) -> str:
4091 """X-axis label."""
4092 return self._com.Name
4094 @property
4095 def unit(self) -> str:
4096 """X-axis unit."""
4097 return self._com.Unit
4100class SignalYAxis:
4101 """Wrapper around a COM signal y-axis descriptor."""
4103 def __init__(self, obj: Any) -> None:
4104 self._com = obj
4106 @property
4107 def max(self) -> float:
4108 """Maximum y-axis value."""
4109 return float(self._com.Max)
4111 @property
4112 def min(self) -> float:
4113 """Minimum y-axis value."""
4114 return float(self._com.Min)
4116 @property
4117 def name(self) -> str:
4118 """Y-axis label."""
4119 return self._com.Name
4121 @property
4122 def unit(self) -> str:
4123 """Y-axis unit."""
4124 return self._com.Unit
4127class SignalDescription:
4128 """Wrapper around COM signal metadata."""
4130 def __init__(self, obj: Any) -> None:
4131 self._com = obj
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]
4138 @property
4139 def complex(self) -> bool:
4140 """Whether the signal is complex-valued."""
4141 return bool(self._com.Complex)
4143 @property
4144 def data_type(self) -> DataType:
4145 """Signal data type."""
4146 return DataType(self._com.DataType)
4148 @property
4149 def db_reference(self) -> float:
4150 """Zero-dB reference value."""
4151 return float(self._com.DbReference)
4153 @property
4154 def domain_type(self) -> DomainType:
4155 """Domain type of the signal."""
4156 return DomainType(self._com.DomainType)
4158 @property
4159 def function_type(self) -> FunctionType:
4160 """Function type of the signal."""
4161 return FunctionType(self._com.FunctionType)
4163 @property
4164 def name(self) -> str:
4165 """Signal description name."""
4166 return self._com.Name
4168 @property
4169 def power_signal(self) -> bool:
4170 """Whether the signal is a power signal."""
4171 return bool(self._com.PowerSignal)
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]
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]
4183 @property
4184 def rms_correction_factor(self) -> float:
4185 """RMS correction factor."""
4186 return float(self._com.RMSCorrectionFactor)
4188 @property
4189 def x_axis(self) -> SignalXAxis:
4190 """X-axis descriptor."""
4191 return SignalXAxis(self._com.XAxis)
4193 @property
4194 def y_axis(self) -> SignalYAxis:
4195 """Y-axis descriptor."""
4196 return SignalYAxis(self._com.YAxis)
4199class DataPoint:
4200 """Wrapper around a COM data point."""
4202 def __init__(self, obj: Any) -> None:
4203 self._com = obj
4205 @property
4206 def average_count(self) -> int:
4207 """Number of traces that contributed to this averaged result."""
4208 return int(self._com.AverageCount)
4210 @property
4211 def meas_point(self) -> MeasPoint:
4212 """Measurement point corresponding to this data point."""
4213 return MeasPoint(self._com.MeasPoint)
4215 @property
4216 def point_domain(self) -> "PointDomain":
4217 """Parent point domain."""
4218 return PointDomain(self._com.PointDomain)
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)))
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 )
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)))
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 )
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)))
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))
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)))
4286class PointDomain:
4287 """Wrapper around a COM point domain."""
4289 def __init__(self, obj: Any) -> None:
4290 self._com = obj
4292 def get_x_axis(self) -> XAxis:
4293 """Return an x-axis descriptor for the point domain."""
4294 return XAxis(self._com.GetXAxis())
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()]
4300 @property
4301 def channels(self) -> list[Channel]:
4302 """Channels in the point domain."""
4303 return [Channel(c) for c in self._com.Channels]
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]
4310 @property
4311 def name(self) -> str:
4312 """Point-domain name."""
4313 return self._com.Name
4315 @property
4316 def rms_correction_factor(self) -> float:
4317 """RMS correction factor for the point domain."""
4318 return float(self._com.RmsCorrectionFactor)
4320 @property
4321 def type(self) -> DomainType:
4322 """Point-domain type."""
4323 return DomainType(self._com.Type)
4326class PolyFile:
4327 """High-level wrapper around a Polytec PSV file COM object."""
4329 def __init__(self, obj_or_path: str | PathLike[str] | Any) -> None:
4330 """Open a Polytec file or wrap an existing COM file object.
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.
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 )
4350 is_dispatch = False
4351 try:
4352 is_dispatch = isinstance(obj_or_path, client.CDispatch)
4353 except Exception:
4354 is_dispatch = False
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))
4362 def __repr__(self) -> str:
4363 """Return a debug representation."""
4364 return f"PolyFile({self.file_name!r})"
4366 @property
4367 def contained_files(self) -> list["PolyFile"]:
4368 """Contained files if this file is a combined file.
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)
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]
4384 @property
4385 def parent_file(self) -> "PolyFile":
4386 """Parent combined file containing this file.
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
4398 @property
4399 def title(self) -> str:
4400 """Document title."""
4401 return self._com.SummaryInfo.Title
4403 @property
4404 def comments(self) -> str:
4405 """Document comments."""
4406 return self._com.SummaryInfo.Comments
4408 @property
4409 def author(self) -> str:
4410 """Document author."""
4411 return self._com.SummaryInfo.Author
4413 @property
4414 def subject(self) -> str:
4415 """Document subject."""
4416 return self._com.SummaryInfo.Subject
4418 @property
4419 def date(self) -> Any:
4420 """File timestamp as returned by the COM interface."""
4421 return self._com.Version.FileTime
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}
4440 @property
4441 def file_name(self) -> str:
4442 """Path of the opened file."""
4443 return self._com.FileName
4445 @property
4446 def acquisition_mode(self) -> AcquisitionMode:
4447 """Active acquisition mode for the file."""
4448 return AcquisitionMode(self._com.Infos.AcquisitionInfoModes.ActiveMode)
4450 @property
4451 def acquisition_properties(self) -> dict[str, Any]:
4452 """Available acquisition-property wrappers for the active mode.
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
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 ]
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
4518 @property
4519 def alignments(self) -> Alignments:
4520 """Alignment information."""
4521 return Alignments(self._com.Infos.Alignments)
4523 @property
4524 def camera_settings(self) -> None:
4525 """Camera settings.
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 )
4538 @property
4539 def elements(self) -> list[Element]:
4540 """Geometry elements."""
4541 return [Element(val) for val in self._com.Infos.Elements]
4543 @property
4544 def hardware(self) -> Hardware:
4545 """Hardware information."""
4546 return Hardware(self._com.Infos.Hardware)
4548 @property
4549 def measurement_points(self) -> MeasPoints:
4550 """Measurement points."""
4551 return MeasPoints(self._com.Infos.MeasPoints)
4553 @property
4554 def measurement_locations(self) -> list[MeasurementLocation]:
4555 """Measurement locations.
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
4570 @property
4571 def profiles(self) -> list[Profile]:
4572 """Profiles defined in the dataset.
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
4584 @property
4585 def scan_head_devices_info(self) -> ScanHeadDevicesInfo:
4586 """Scan-head-device information."""
4587 return ScanHeadDevicesInfo(self._com.Infos.ScanHeadDevicesInfo)
4589 @property
4590 def spectrogram_info(self) -> SpectrogramInfo:
4591 """Spectrogram information."""
4592 return SpectrogramInfo(self._com.Infos.SpectrogramInfo)
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]
4599 @property
4600 def video_bitmap(self) -> VideoBitmap:
4601 """Video bitmap information."""
4602 return VideoBitmap(self._com.Infos.VideoBitmap)
4604 @property
4605 def video_mapping_info(self) -> VideoMappingInfo:
4606 """Video mapping information."""
4607 return VideoMappingInfo(self._com.Infos.VideoMappingInfo)
4609 @property
4610 def point_domains(self) -> list[PointDomain]:
4611 """Available point domains."""
4612 return [PointDomain(v) for v in self._com.GetPointDomains()]
4614 def _get_single_point_domain(self, domain_type: DomainType) -> PointDomain:
4615 """Return the single point domain of the requested type.
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]
4629 @staticmethod
4630 def _channel_is_reference(channel: Channel) -> bool:
4631 """Heuristic check for whether a channel is a reference channel.
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()
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 )
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 )
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 )
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)
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)
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.
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.
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()
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 )
4743 domain = self._get_single_point_domain(DomainType.SPECTRUM)
4744 data_points = domain.data_points
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] = []
4756 for channel in domain.channels:
4757 if VERBOSE:
4758 print(f"Looking at Channel {channel.name}")
4760 for signal in channel.signals:
4761 if VERBOSE:
4762 print(f" Looking at Signal {signal.name}")
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
4768 if VERBOSE:
4769 print(" This signal is an FRF")
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
4777 response_dof = self._single_response_dof(description, channel.name, signal.name)
4778 reference_dof = self._single_reference_dof(description, channel.name, signal.name)
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 )
4785 if VERBOSE:
4786 print(" Degrees of Freedom")
4787 print(f" Response: {response_coord}")
4788 print(f" Reference: {reference_coord}")
4790 this_ordinate: list[np.ndarray] = []
4791 this_measpoint_coords: list[Any] = []
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)
4802 this_ordinate_arr = np.asarray(this_ordinate)
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 )
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 )
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 )
4893 def get_coherence(self, collapse_reference_signals: bool = True) -> CoherenceArray:
4894 """Extract coherence functions from the active spectrum point domain.
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.
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
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] = []
4921 for channel in domain.channels:
4922 if VERBOSE:
4923 print(f"Looking at Channel {channel.name}")
4925 for signal in channel.signals:
4926 if VERBOSE:
4927 print(f" Looking at Signal {signal.name}")
4929 if signal.name.lower() != "coherence":
4930 continue
4932 if VERBOSE:
4933 print(" This signal is coherence")
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
4941 response_dof = self._single_response_dof(description, channel.name, signal.name)
4942 reference_dof = self._single_reference_dof(description, channel.name, signal.name)
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 )
4949 if VERBOSE:
4950 print(" Degrees of Freedom")
4951 print(f" Response: {response_coord}")
4952 print(f" Reference: {reference_coord}")
4954 this_ordinate: list[np.ndarray] = []
4955 this_measpoint_coords: list[Any] = []
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)
4965 this_ordinate_arr = np.asarray(this_ordinate)
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 )
5030 if not ordinates:
5031 raise ValueError("No coherence signals found in this dataset.")
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 )
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.
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.
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
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] = []
5083 for channel in domain.channels:
5084 if VERBOSE:
5085 print(f"Looking at Channel {channel.name}")
5087 for signal in channel.signals:
5088 if VERBOSE:
5089 print(f" Looking at Signal {signal.name}")
5091 if signal.name.lower() != "multiple coherence":
5092 continue
5094 if VERBOSE:
5095 print(" This signal is multiple coherence")
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
5103 response_dof = self._single_response_dof(description, channel.name, signal.name)
5104 response_coord = coordinate_array(response_dof.node, response_dof.direction.value)
5106 if VERBOSE:
5107 print(" Degrees of Freedom")
5108 print(f" Response: {response_coord}")
5110 this_ordinate: list[np.ndarray] = []
5111 this_measpoint_coords: list[Any] = []
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)
5121 this_ordinate_arr = np.asarray(this_ordinate)
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 )
5183 if not ordinates:
5184 raise ValueError("No multiple coherence signals found in this dataset.")
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 )
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.
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.
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 )
5230 domain = self._get_single_point_domain(DomainType.SPECTRUM)
5231 data_points = domain.data_points
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] = []
5243 for channel in domain.channels:
5244 if VERBOSE:
5245 print(f"Looking at Channel {channel.name}")
5247 for signal in channel.signals:
5248 if VERBOSE:
5249 print(f" Looking at Signal {signal.name}")
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
5261 if VERBOSE:
5262 print(" This signal is a PSD")
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
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
5276 if VERBOSE:
5277 print(" Degrees of Freedom")
5278 print(f" Response: {response_coord}")
5279 print(f" Reference: {reference_coord}")
5281 this_ordinate: list[np.ndarray] = []
5282 this_measpoint_coords: list[Any] = []
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)
5292 this_ordinate_arr = np.asarray(this_ordinate)
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 )
5357 if not ordinates:
5358 raise ValueError("No PSD signals found in this dataset.")
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 )
5380 def get_time_response(self, collapse_reference_signals: bool = True) -> TimeHistoryArray:
5381 """Extract time histories from the active time-domain point domain.
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.
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
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] = []
5408 for channel in domain.channels:
5409 if VERBOSE:
5410 print(f"Looking at Channel {channel.name}")
5412 for signal in channel.signals:
5413 if VERBOSE:
5414 print(f" Looking at Signal {signal.name}")
5416 if "voltage" in signal.name.lower():
5417 continue
5419 if VERBOSE:
5420 print(" This signal is time data")
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
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)
5433 if VERBOSE:
5434 print(" Degrees of Freedom")
5435 print(f" Response: {response_coord}")
5437 this_ordinate: list[np.ndarray] = []
5438 this_measpoint_coords: list[Any] = []
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)
5448 this_ordinate_arr = np.asarray(this_ordinate)
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 )
5510 if not ordinates:
5511 raise ValueError("No time-response signals found in this dataset.")
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 )
5527 def get_geometry(self) -> Geometry:
5528 """Build an ``sdynpy`` geometry object from measurement points and elements.
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)
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)
5550 return geometry
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
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 )
5575 kwargs = dict(kwargs)
5576 if "ax" in kwargs:
5577 kwargs["ax"] = None
5579 out: list[Any] = []
5580 for child in contained:
5581 method = getattr(child, method_name)
5582 out.append(method(*args, **kwargs))
5583 return out
5585 def get_image(self) -> Image.Image | list[Any]:
5586 """Return the stored test image as a PIL image.
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")
5594 return self.video_bitmap.get_image().copy()
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)
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
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"]
5630 @staticmethod
5631 def _default_alignment_markers() -> list[str]:
5632 """Default marker cycle for plotting multiple alignments."""
5633 return ["o", "x", "^", "s", "d", "+", "*"]
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 ]
5651 @staticmethod
5652 def _default_status_markers() -> list[str]:
5653 """Default marker cycle for status plotting."""
5654 return ["o", "x", "^", "s", "d", "+", "*", "v", "<", ">"]
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)
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"
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)
5679 return " | ".join(names) if names else str(int(value))
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()
5709 if video_rect is None:
5710 raise ValueError("`video_rect` must be provided.")
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 ]
5717 if label_color is None:
5718 label_color = color
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)
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
5740 if mode.lower() == "matplotlib":
5742 created_fig = False
5743 if ax is None:
5744 fig, ax = plt.subplots()
5745 created_fig = True
5746 else:
5747 fig = ax.figure
5749 if not ax.images:
5750 ax.imshow(image)
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)
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)
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")
5766 if title is not None:
5767 ax.set_title(title)
5769 if created_fig:
5770 fig.tight_layout()
5772 return fig, ax
5774 raise ValueError("`mode` must be either 'pil' or 'matplotlib'.")
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.
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 )
5808 image = self.get_image()
5809 video_rect = tuple(float(v) for v in self.measurement_points.get_video_rect())
5811 points_xy: list[tuple[float, float]] = []
5812 labels: list[str] = []
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))
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 )
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.
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 )
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])]
5875 image = self.get_image()
5876 video_rect = tuple(float(v) for v in self.alignments.get_video_rect())
5878 default_colors = self._default_alignment_colors()
5879 default_markers = self._default_alignment_markers()
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 )
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))]
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
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)
5924 image_width, image_height = image.size
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 )
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]
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)
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)
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")
5959 if title is not None:
5960 ax.set_title(title)
5962 if alignment_index is None and len(selected_alignments) > 1:
5963 ax.legend()
5965 if created_fig:
5966 fig.tight_layout()
5968 return fig, ax
5970 raise ValueError("`mode` must be either 'pil' or 'matplotlib'.")
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.
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 )
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])]
6012 image = self.get_image()
6013 video_rect = tuple(float(v) for v in self.alignments.get_video_rect())
6015 default_colors = self._default_alignment_colors()
6016 default_markers = self._default_alignment_markers()
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 )
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))]
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
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)
6064 image_width, image_height = image.size
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 )
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]
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)
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)
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")
6103 if title is not None:
6104 ax.set_title(title)
6106 if alignment_index is None and len(selected_alignments) > 1:
6107 ax.legend()
6109 if created_fig:
6110 fig.tight_layout()
6112 return fig, ax
6114 raise ValueError("`mode` must be either 'pil' or 'matplotlib'.")
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 )
6137 video_rect = tuple(float(v) for v in self.measurement_points.get_video_rect())
6139 grouped_points: dict[int, list[tuple[float, float]]] = defaultdict(list)
6140 grouped_labels: dict[int, list[str]] = defaultdict(list)
6142 for pt in self.measurement_points:
6143 try:
6144 x, y = pt.video_xy()
6145 except Exception:
6146 continue
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))
6152 if not grouped_points:
6153 raise ValueError(f"No points with plottable {status_attr} data were found.")
6155 unique_status_values = sorted(grouped_points.keys())
6157 colors = self._default_status_colors()
6158 markers = self._default_status_markers()
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
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
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)
6193 image_width, image_height = image.size
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)]
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]
6206 legend_label = self._decode_flag_label(status_value, enum_cls)
6208 ax.scatter(
6209 xs,
6210 ys,
6211 c=this_color,
6212 marker=this_marker,
6213 s=size,
6214 label=legend_label,
6215 )
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)
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")
6228 if title is not None:
6229 ax.set_title(title)
6231 ax.legend()
6233 if created_fig:
6234 fig.tight_layout()
6236 return fig, ax
6238 raise ValueError("`mode` must be either 'pil' or 'matplotlib'.")
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.
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 )
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 )
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.
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 )
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 )
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.
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 )
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 )
6360 def get_alignment3d_quality(self):
6361 """Returns the alignment quality for each laser head.
6363 If the file contains multiple files, then one quality score
6364 will be returned for each contained file.
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)
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
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.
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 )
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 )
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 )
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.
6433 Parameters
6434 ----------
6435 x_pixel, y_pixel
6436 Image pixel coordinates.
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``
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 )
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 )
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 )
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
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 )
6489 return results
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.
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.
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``
6522 For a combined file, returns a list of per-contained-file results.
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 )
6540 alignments_2d = self.alignments.alignments_2d
6541 alignments_3d = self.alignments.alignments_3d
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 )
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 )
6555 results: list[dict[str, Any]] = []
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]
6562 p0 = alignment3d.scanner_to_coord_3d(scanner_x, scanner_y, distance0)
6563 p1 = alignment3d.scanner_to_coord_3d(scanner_x, scanner_y, distance1)
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
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
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 )
6592 return results
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.
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.
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``
6625 For a combined file, returns a list of per-contained-file results.
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 )
6642 beam_rays = self.get_beam_rays_at_pixel(
6643 x_pixel,
6644 y_pixel,
6645 distance0=distance0,
6646 distance1=distance1,
6647 )
6649 if not isinstance(beam_rays, list):
6650 raise ValueError("Unexpected result from get_beam_rays_at_pixel.")
6652 if len(beam_rays) < 2:
6653 raise ValueError(
6654 "At least two beam rays are required to compute an intersection."
6655 )
6657 try:
6658 video_xy = beam_rays[0]["video_xy"]
6659 except Exception:
6660 video_xy = self.pixel_to_video(x_pixel, y_pixel)
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
6666 intersection = np.asarray(
6667 intersection_point_multiple_lines(P0, P1),
6668 dtype=float,
6669 ).reshape(3)
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 )
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 }
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 )
6703 video_rect = tuple(float(v) for v in self.measurement_points.get_video_rect())
6704 image_width, image_height = image.size
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 )
6722 if not pixel_points:
6723 return np.empty((0, 2), dtype=float)
6725 return np.asarray(pixel_points, dtype=float)
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.
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.
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
6752 from scipy.spatial import ConvexHull
6753 from matplotlib.path import Path
6755 hull = ConvexHull(pixel_points)
6756 hull_points = pixel_points[hull.vertices]
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]
6767 return Path(hull_points)
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()))
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.
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.
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``
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 )
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'.")
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'.")
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 )
6859 if pixel_step <= 0:
6860 raise ValueError("`pixel_step` must be a positive integer.")
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 )
6869 image_width, image_height = image.size
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)
6878 xx, yy = np.meshgrid(x_vals, y_vals)
6879 error = np.full(xx.shape, np.nan, dtype=float)
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 )
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)
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
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
6907 x_pixel = float(xx[iy, ix])
6908 y_pixel = float(yy[iy, ix])
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
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)
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))
6943 hull_points = None
6944 if hull_path is not None:
6945 hull_points = np.asarray(hull_path.vertices, dtype=float)
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 }
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.
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.
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 )
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 )
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.")
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"]
7066 created_fig = False
7067 if ax is None:
7068 fig, ax = plt.subplots()
7069 created_fig = True
7070 else:
7071 fig = ax.figure
7073 ax.imshow(image)
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")
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 )
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 )
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")
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)
7126 if show_measurement_points or (show_hull and hull_points is not None):
7127 ax.legend()
7129 if created_fig:
7130 fig.tight_layout()
7132 return fig, ax, result