pyPRB 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyPRB/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ """
2
+ Rotor Balancing Package
3
+ =======================
4
+
5
+ A Python package for static and dynamic rotor balancing calculations based on
6
+ the influence coefficient method described in "Static and Dynamic Balancing
7
+ using portable measuring equipment" by John Vaughan, Bruel & Kjaer.
8
+
9
+ Classes
10
+ -------
11
+ VibrationVector
12
+ Represents vibration measurements as vectors with amplitude and phase.
13
+
14
+ MassVector
15
+ Represents a mass placed at an angular position on a rotor.
16
+
17
+ StaticBalancing
18
+ Performs single-plane (static) balancing calculations.
19
+
20
+ DynamicBalancing
21
+ Performs two-plane (dynamic) balancing calculations.
22
+
23
+ Examples
24
+ --------
25
+ Static balancing:
26
+
27
+ >>> from pyPRB import VibrationVector, MassVector, StaticBalancing
28
+ >>> V_0 = VibrationVector(3.4, 116) # initial vibration vector
29
+ >>> V_1 = VibrationVector(1.8, 42) # vibration vector after trial mass
30
+ >>> trial_mass = MassVector(2.0, 0.0)
31
+ >>> balancer = StaticBalancing(V_0, V_1, trial_mass=trial_mass)
32
+ >>> comp_mass = balancer.compute_compensation()
33
+
34
+ Dynamic balancing:
35
+
36
+ >>> from pyPRB import VibrationVector, MassVector, DynamicBalancing
37
+ >>> V_1_0 = VibrationVector(7.2, 238)
38
+ >>> V_2_0 = VibrationVector(13.5, 296)
39
+ >>> V_1_1 = VibrationVector(4.9, 114)
40
+ >>> V_2_1 = VibrationVector(9.2, 347)
41
+ >>> V_1_2 = VibrationVector(4.0, 79)
42
+ >>> V_2_2 = VibrationVector(12.0, 292)
43
+ >>> trial_mass_1 = MassVector(2.5, 0.0)
44
+ >>> trial_mass_2 = MassVector(2.5, 0.0)
45
+ >>> balancer = DynamicBalancing(V_1_0, V_2_0, V_1_1, V_2_1,
46
+ ... V_1_2, V_2_2,
47
+ ... trial_mass_1=trial_mass_1,
48
+ ... trial_mass_2=trial_mass_2)
49
+ >>> comp_mass_1, comp_mass_2 = balancer.compute_compensation()
50
+ """
51
+
52
+ from .core import VibrationVector, MassVector, StaticBalancing, DynamicBalancing
53
+ from .processing import VibrationExtractor, IQDemodulation, LeastSquaresFit, SynchronousAveraging, FFTExtraction, detect_rotation_freq
54
+ from .__version__ import __version__, __author__, __email__
55
+
56
+ # Define what gets imported with "from pyPRB import *"
57
+ __all__ = [
58
+ 'VibrationVector',
59
+ 'MassVector',
60
+ 'StaticBalancing',
61
+ 'DynamicBalancing',
62
+ 'VibrationExtractor',
63
+ 'IQDemodulation',
64
+ 'LeastSquaresFit',
65
+ 'SynchronousAveraging',
66
+ 'FFTExtraction',
67
+ 'detect_rotation_freq',
68
+ '__version__',
69
+ ]
pyPRB/__version__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Version information for pyPRB package."""
2
+
3
+ __version__ = '1.0.0'
4
+ __author__ = 'Gašper Krivic'
5
+ __email__ = 'gasper.krivic@fs.uni-lj.si'
pyPRB/core.py ADDED
@@ -0,0 +1,486 @@
1
+ """
2
+ Core classes for rotor balancing calculations.
3
+
4
+ Based on "Static and Dynamic Balancing using portable measuring equipment"
5
+ by John Vaughan, Brüel & Kjær.
6
+
7
+ Angle convention
8
+ ----------------
9
+ All angles follow the rotor datum convention: 0° is at the trigger/datum
10
+ mark on the rotor, and positive angles are measured in the direction of
11
+ rotation.
12
+ """
13
+
14
+ import numpy as np
15
+ from tabulate import tabulate
16
+
17
+
18
+ class VibrationVector:
19
+ """Represents a vibration vector in 2D space.
20
+
21
+ A vibration vector is characterized by an amplitude and phase angle,
22
+ which can be used to represent unbalance in rotating machinery.
23
+
24
+ Parameters
25
+ ----------
26
+ amplitude : float
27
+ The magnitude of the vibration vector.
28
+ phase : float
29
+ The phase angle in degrees.
30
+
31
+ Notes
32
+ -----
33
+ Angles use the rotor datum convention: 0° is at the trigger/datum mark,
34
+ positive angles are in the direction of rotation.
35
+
36
+ Examples
37
+ --------
38
+ >>> v1 = VibrationVector(10.5, 45)
39
+ >>> v2 = VibrationVector(5.2, 135)
40
+ >>> v3 = v1 + v2
41
+ >>> print(v3)
42
+ VibrationVector(amplitude=11.7171, phase=71.3464°)
43
+ """
44
+
45
+ def __init__(self, amplitude: float, phase: float):
46
+ """Initialize the vibration vector with amplitude and phase (in degrees)."""
47
+ if amplitude < 0:
48
+ raise ValueError("Amplitude must be non-negative.")
49
+
50
+ self.amplitude = amplitude
51
+ self.phase = phase % 360
52
+ self.complex_num = amplitude * np.exp(1j * np.deg2rad(self.phase))
53
+
54
+ @classmethod
55
+ def from_complex(cls, z: complex) -> 'VibrationVector':
56
+ """Create a VibrationVector from a complex number.
57
+
58
+ Parameters
59
+ ----------
60
+ z : complex
61
+ Complex number representation of the vector.
62
+
63
+ Returns
64
+ -------
65
+ VibrationVector
66
+ A new VibrationVector instance.
67
+ """
68
+ amplitude = np.abs(z)
69
+ phase = np.rad2deg(np.angle(z))
70
+ return cls(amplitude, phase)
71
+
72
+ def __repr__(self) -> str:
73
+ return f'VibrationVector(amplitude={self.amplitude:g}, phase={self.phase:g}°)'
74
+
75
+ def __add__(self, other: 'VibrationVector') -> 'VibrationVector':
76
+ """Add two vibration vectors."""
77
+ z1 = self.complex_num
78
+ z2 = other.complex_num
79
+ z_sum = z1 + z2
80
+ return VibrationVector.from_complex(z_sum)
81
+
82
+ def __sub__(self, other: 'VibrationVector') -> 'VibrationVector':
83
+ """Subtract two vibration vectors."""
84
+ z1 = self.complex_num
85
+ z2 = other.complex_num
86
+ z_diff = z1 - z2
87
+ return VibrationVector.from_complex(z_diff)
88
+
89
+ def __mul__(self, other: 'float | VibrationVector') -> 'VibrationVector':
90
+ """Multiply vibration vector by scalar or another vector."""
91
+ if isinstance(other, (int, float)):
92
+ if other >= 0:
93
+ return VibrationVector(self.amplitude * other, self.phase)
94
+ elif other < 0:
95
+ return VibrationVector(self.amplitude * (-other),
96
+ self.phase + 180)
97
+ elif isinstance(other, VibrationVector):
98
+ z1 = self.complex_num
99
+ z2 = other.complex_num
100
+ z_prod = z1 * z2
101
+ return VibrationVector.from_complex(z_prod)
102
+ else:
103
+ return NotImplemented
104
+
105
+ def __rmul__(self, other: float) -> 'VibrationVector':
106
+ """Right multiplication (allows scalar * vector)."""
107
+ return self.__mul__(other)
108
+
109
+ def __truediv__(self, other: 'float | VibrationVector') -> 'VibrationVector':
110
+ """Divide vibration vector by scalar or another vector."""
111
+ if isinstance(other, (int, float)):
112
+ if other > 0:
113
+ return VibrationVector(self.amplitude / other, self.phase)
114
+ elif other < 0:
115
+ return VibrationVector(self.amplitude / (-other),
116
+ self.phase + 180)
117
+ else:
118
+ raise ZeroDivisionError("division by zero")
119
+ elif isinstance(other, VibrationVector):
120
+ z1 = self.complex_num
121
+ z2 = other.complex_num
122
+ z_div = z1 / z2
123
+ return VibrationVector.from_complex(z_div)
124
+ else:
125
+ return NotImplemented
126
+
127
+ def __neg__(self) -> 'VibrationVector':
128
+ """Negate the vibration vector (180° phase shift)."""
129
+ return VibrationVector(self.amplitude, self.phase + 180)
130
+
131
+
132
+ class MassVector:
133
+ """Represents a mass vector with amplitude, angular position, and units.
134
+
135
+ A mass vector describes a mass placed at a specific angular position
136
+ on a rotor. Used to represent both trial masses and compensation masses
137
+ in balancing calculations.
138
+
139
+ Parameters
140
+ ----------
141
+ amplitude : float
142
+ The mass value (must be non-negative).
143
+ phase : float
144
+ The angular position on the rotor in degrees.
145
+ units : str, optional
146
+ The units of the mass value, by default "g" (grams).
147
+ For display only — no unit conversion is performed.
148
+
149
+ Notes
150
+ -----
151
+ Phase represents the angular position on the rotor where the mass is
152
+ (or should be) mounted. Angles use the rotor datum convention: 0° is at
153
+ the trigger/datum mark, positive angles are in the direction of rotation.
154
+
155
+ Examples
156
+ --------
157
+ >>> trial = MassVector(2.5, 45)
158
+ >>> print(trial)
159
+ MassVector(amplitude=2.5, phase=45°, units='g')
160
+
161
+ >>> comp = MassVector(3.0, -30, units='oz')
162
+ >>> print(comp)
163
+ MassVector(amplitude=3, phase=-30°, units='oz')
164
+ """
165
+
166
+ def __init__(self, amplitude: float, phase: float, units: str = "g"):
167
+ """Initialize the mass vector with amplitude, phase, and units."""
168
+ if amplitude <= 0:
169
+ raise ValueError("Amplitude must be positive.")
170
+ if not isinstance(units, str):
171
+ raise TypeError("Units must be a string.")
172
+
173
+ self.amplitude = amplitude
174
+ self.phase = phase % 360
175
+ if self.phase > 180:
176
+ self.phase -= 360
177
+ self.units = units
178
+
179
+ def __repr__(self) -> str:
180
+ return (f"MassVector(amplitude={self.amplitude:g}, "
181
+ f"phase={self.phase:g}°, units='{self.units}')")
182
+
183
+
184
+ class StaticBalancing:
185
+ """Performs static (single-plane) balancing calculations.
186
+
187
+ Static balancing is used when the rotor can be treated as having all
188
+ its mass in a single plane, such as narrow disks, flywheels, or grindstones.
189
+
190
+ Parameters
191
+ ----------
192
+ V_0 : VibrationVector
193
+ Initial vibration measurement (no trial mass).
194
+ V_1 : VibrationVector
195
+ Vibration measurement with trial mass attached.
196
+ trial_mass : MassVector
197
+ Trial mass used, with amplitude (mass value) and phase (angular
198
+ position on the rotor where it was mounted).
199
+
200
+ Examples
201
+ --------
202
+ >>> V_0 = VibrationVector(3.4, 116)
203
+ >>> V_1 = VibrationVector(1.8, 42)
204
+ >>> trial_mass = MassVector(2.0, 0.0)
205
+ >>> balancer = StaticBalancing(V_0, V_1, trial_mass=trial_mass)
206
+ >>> comp_mass = balancer.compute_compensation()
207
+ Compensation mass: 2.01 g at position -30.8° on the rotor.
208
+ """
209
+
210
+ def __init__(self, V_0: VibrationVector, V_1: VibrationVector,
211
+ trial_mass: MassVector):
212
+ self.V_0 = V_0
213
+ self.V_1 = V_1
214
+ self.trial_mass = trial_mass
215
+
216
+ def __repr__(self) -> str:
217
+ """Return a formatted table representation of the balancing data.
218
+ """
219
+ # Create data rows
220
+ tm = self.trial_mass
221
+ data = [[
222
+ "None", f"{self.V_0.amplitude:g}", f"{self.V_0.phase:g}°", "V_0"
223
+ ],
224
+ [
225
+ f"{tm.amplitude:.1f} {tm.units} @ {tm.phase:g}°",
226
+ f"{self.V_1.amplitude:g}",
227
+ f"{self.V_1.phase:g}°", "V_1"
228
+ ]]
229
+
230
+ # Column headers
231
+ headers = ["Trial Mass", "Amplitude", "Phase", "Vector"]
232
+
233
+ # Generate table similar to B&K document
234
+ table = tabulate(data,
235
+ headers=headers,
236
+ tablefmt="grid",
237
+ stralign="center",
238
+ numalign="center")
239
+
240
+ # Add title
241
+ title = "Static Balancing - Measured Vibration Data"
242
+ separator = "=" * 80
243
+
244
+ return f"\n{separator}\n{title}\n{separator}\n{table}\n{separator}"
245
+
246
+ def _compute_V_T(self) -> VibrationVector:
247
+ """Compute the effect of the trial mass.
248
+
249
+ Returns
250
+ -------
251
+ VibrationVector
252
+ The vibration vector caused by the trial mass alone.
253
+ """
254
+ V_T = self.V_1 - self.V_0
255
+ self.V_T = V_T
256
+ return V_T
257
+
258
+ def compute_compensation(self, repr: bool = True) -> MassVector:
259
+ """Calculate the compensation mass and its angular position.
260
+
261
+ Parameters
262
+ ----------
263
+ repr : bool, optional
264
+ If ``True``, print the compensation details. Default is ``True``.
265
+
266
+ Returns
267
+ -------
268
+ MassVector
269
+ Compensating mass with absolute angular position on the rotor.
270
+ """
271
+ V_T = self._compute_V_T()
272
+ compensation_mass = (self.V_0.amplitude / V_T.amplitude
273
+ * self.trial_mass.amplitude)
274
+ relative_position = -V_T.phase + self.V_0.phase + 180
275
+ absolute_position = relative_position + self.trial_mass.phase
276
+ result = MassVector(compensation_mass, absolute_position,
277
+ self.trial_mass.units)
278
+ if repr:
279
+ print(
280
+ f"Compensation mass: {result.amplitude:.2f} {result.units} "
281
+ f"at position {result.phase:.1f}° on the rotor.")
282
+
283
+ return result
284
+
285
+
286
+ class DynamicBalancing:
287
+ """Performs dynamic (two-plane) balancing calculations.
288
+
289
+ Dynamic balancing is required for rotors with significant length, where
290
+ unbalance exists in multiple planes. This class implements the two-plane
291
+ influence coefficient method.
292
+
293
+ Parameters
294
+ ----------
295
+ V_1_0 : VibrationVector
296
+ Plane 1 vibration, initial run (no trial masses).
297
+ V_2_0 : VibrationVector
298
+ Plane 2 vibration, initial run (no trial masses).
299
+ V_1_1 : VibrationVector
300
+ Plane 1 vibration with trial mass in plane 1.
301
+ V_2_1 : VibrationVector
302
+ Plane 2 vibration with trial mass in plane 1.
303
+ V_1_2 : VibrationVector
304
+ Plane 1 vibration with trial mass in plane 2.
305
+ V_2_2 : VibrationVector
306
+ Plane 2 vibration with trial mass in plane 2.
307
+ trial_mass_1 : MassVector
308
+ Trial mass used in plane 1, with amplitude (mass value) and phase
309
+ (angular position on the rotor where it was mounted).
310
+ trial_mass_2 : MassVector
311
+ Trial mass used in plane 2, with amplitude (mass value) and phase
312
+ (angular position on the rotor where it was mounted).
313
+
314
+ Notes
315
+ -----
316
+ The naming convention V_i_j represents:
317
+ - i: measurement plane (1 or 2)
318
+ - j: run number (0=initial, 1=trial mass in plane 1, 2=trial mass in plane 2)
319
+
320
+ Trial masses in both planes must use the same units. No unit conversion
321
+ is performed.
322
+
323
+ Examples
324
+ --------
325
+ >>> # Initial measurements
326
+ >>> V_1_0 = VibrationVector(7.2, 238)
327
+ >>> V_2_0 = VibrationVector(13.5, 296)
328
+ >>> # With trial mass in plane 1
329
+ >>> V_1_1 = VibrationVector(4.9, 114)
330
+ >>> V_2_1 = VibrationVector(9.2, 347)
331
+ >>> # With trial mass in plane 2
332
+ >>> V_1_2 = VibrationVector(4.0, 79)
333
+ >>> V_2_2 = VibrationVector(12.0, 292)
334
+ >>>
335
+ >>> trial_mass_1 = MassVector(2.5, 0.0)
336
+ >>> trial_mass_2 = MassVector(2.5, 0.0)
337
+ >>> balancer = DynamicBalancing(V_1_0, V_2_0, V_1_1, V_2_1,
338
+ ... V_1_2, V_2_2,
339
+ ... trial_mass_1=trial_mass_1,
340
+ ... trial_mass_2=trial_mass_2)
341
+ >>> comp_mass_1, comp_mass_2 = balancer.compute_compensation(repr=False)
342
+ >>> print(comp_mass_1)
343
+ MassVector(amplitude=2.95138, phase=50.1889°, units='g')
344
+ >>> print(comp_mass_2)
345
+ MassVector(amplitude=2.84414, phase=-81.8841°, units='g')
346
+ """
347
+
348
+ def __init__(self, V_1_0: VibrationVector, V_2_0: VibrationVector,
349
+ V_1_1: VibrationVector, V_2_1: VibrationVector,
350
+ V_1_2: VibrationVector, V_2_2: VibrationVector,
351
+ trial_mass_1: MassVector, trial_mass_2: MassVector):
352
+ if trial_mass_1.units != trial_mass_2.units:
353
+ raise ValueError(
354
+ f"Trial mass units must match: got '{trial_mass_1.units}' "
355
+ f"and '{trial_mass_2.units}'. No unit conversion is performed.")
356
+ self.V_1_0 = V_1_0
357
+ self.V_2_0 = V_2_0
358
+ self.V_1_1 = V_1_1
359
+ self.V_2_1 = V_2_1
360
+ self.V_1_2 = V_1_2
361
+ self.V_2_2 = V_2_2
362
+ self.trial_mass_1 = trial_mass_1
363
+ self.trial_mass_2 = trial_mass_2
364
+
365
+ def __repr__(self) -> str:
366
+ """Return a formatted table representation of the balancing data.
367
+ """
368
+ # Create data rows
369
+ tm1 = self.trial_mass_1
370
+ tm2 = self.trial_mass_2
371
+ data = [
372
+ [
373
+ "None", f"{self.V_1_0.amplitude:g}", f"{self.V_1_0.phase:g}°",
374
+ "V_1_0", f"{self.V_2_0.amplitude:g}", f"{self.V_2_0.phase:g}°",
375
+ "V_2_0"
376
+ ],
377
+ [
378
+ f"{tm1.amplitude:g} {tm1.units} @ {tm1.phase:g}° on Plane 1",
379
+ f"{self.V_1_1.amplitude:g}",
380
+ f"{self.V_1_1.phase:g}°", "V_1_1", f"{self.V_2_1.amplitude:g}",
381
+ f"{self.V_2_1.phase:g}°", "V_2_1"
382
+ ],
383
+ [
384
+ f"{tm2.amplitude:g} {tm2.units} @ {tm2.phase:g}° on Plane 2",
385
+ f"{self.V_1_2.amplitude:g}",
386
+ f"{self.V_1_2.phase:g}°", "V_1_2", f"{self.V_2_2.amplitude:g}",
387
+ f"{self.V_2_2.phase:g}°", "V_2_2"
388
+ ],
389
+ ]
390
+
391
+ # Column headers
392
+ headers = [
393
+ "Trial Mass\nSize and Location", "Amplitude\n(Plane 1)",
394
+ "Phase\n(Plane 1)", "Vector", "Amplitude\n(Plane 2)",
395
+ "Phase\n(Plane 2)", "Vector"
396
+ ]
397
+
398
+ # Generate table similar to B&K document
399
+ table = tabulate(data,
400
+ headers=headers,
401
+ tablefmt="grid",
402
+ stralign="center",
403
+ numalign="center")
404
+
405
+ # Add title
406
+ title = "Dynamic Balancing - Measured Vibration Data"
407
+ separator = "=" * 80
408
+
409
+ return f"\n{separator}\n{title}\n{separator}\n{table}\n{separator}"
410
+
411
+ def _compute_Q1(self) -> VibrationVector:
412
+ """Calculate the influence coefficient Q1 for plane 1.
413
+
414
+ Returns
415
+ -------
416
+ VibrationVector
417
+ The Q1 influence coefficient vector.
418
+ """
419
+ A = self.V_1_1 - self.V_1_0
420
+ B = self.V_1_2 - self.V_1_0
421
+ C = self.V_2_1 - self.V_2_0
422
+ D = self.V_2_2 - self.V_2_0
423
+
424
+ Q1 = (B * self.V_2_0 - D * self.V_1_0) / (A * D - B * C)
425
+ return Q1
426
+
427
+ def _compute_Q2(self) -> VibrationVector:
428
+ """Calculate the influence coefficient Q2 for plane 2.
429
+
430
+ Returns
431
+ -------
432
+ VibrationVector
433
+ The Q2 influence coefficient vector.
434
+ """
435
+ A = self.V_1_1 - self.V_1_0
436
+ B = self.V_1_2 - self.V_1_0
437
+ C = self.V_2_1 - self.V_2_0
438
+ D = self.V_2_2 - self.V_2_0
439
+
440
+ Q2 = (-A * self.V_2_0 + C * self.V_1_0) / (A * D - B * C)
441
+ return Q2
442
+
443
+ def compute_compensation(self,
444
+ repr: bool = True
445
+ ) -> tuple[MassVector, MassVector]:
446
+ """Calculate compensating masses and positions for both planes.
447
+
448
+ Parameters
449
+ ----------
450
+ repr : bool, optional
451
+ If ``True``, print the compensation details. Default is ``True``.
452
+
453
+ Returns
454
+ -------
455
+ comp_1 : MassVector
456
+ Compensating mass for plane 1 with absolute angular position.
457
+ comp_2 : MassVector
458
+ Compensating mass for plane 2 with absolute angular position.
459
+ """
460
+ Q1 = self._compute_Q1()
461
+ Q2 = self._compute_Q2()
462
+
463
+ compensation_mass_1 = Q1.amplitude * self.trial_mass_1.amplitude
464
+ position_1 = Q1.phase + self.trial_mass_1.phase
465
+ comp_1 = MassVector(compensation_mass_1, position_1,
466
+ self.trial_mass_1.units)
467
+
468
+ compensation_mass_2 = Q2.amplitude * self.trial_mass_2.amplitude
469
+ position_2 = Q2.phase + self.trial_mass_2.phase
470
+ comp_2 = MassVector(compensation_mass_2, position_2,
471
+ self.trial_mass_2.units)
472
+
473
+ if repr:
474
+ data = [["1", f"{comp_1.amplitude:.2f} {comp_1.units}",
475
+ f"{comp_1.phase:.1f}°"],
476
+ ["2", f"{comp_2.amplitude:.2f} {comp_2.units}",
477
+ f"{comp_2.phase:.1f}°"]]
478
+ headers = ["Plane", "Compensation Mass", "Position"]
479
+ table = tabulate(data,
480
+ headers=headers,
481
+ tablefmt="grid",
482
+ stralign="center",
483
+ numalign="center")
484
+ print(table)
485
+
486
+ return comp_1, comp_2
@@ -0,0 +1,32 @@
1
+ """Signal processing for extracting vibration vectors from time-domain data.
2
+
3
+ Typical workflow
4
+ ----------------
5
+ 1. Determine the rotation frequency — either from machine specifications
6
+ or by calling :func:`detect_rotation_freq` on the trigger signal.
7
+ 2. Create a :class:`VibrationExtractor` with the sample rate and
8
+ rotation frequency.
9
+ 3. Call :meth:`~VibrationExtractor.get_vibration_vector` for each
10
+ measurement run to obtain a :class:`~pyPRB.core.VibrationVector`.
11
+
12
+ The default extraction method is :class:`IQDemodulation`, which
13
+ multiplies the signal by cosine/sine references at the rotation frequency
14
+ and averages to extract the fundamental component. Custom methods can be
15
+ provided via the :class:`ExtractionMethod` protocol.
16
+ """
17
+
18
+ from ._vibration_extractor import ExtractionMethod, VibrationExtractor, detect_rotation_freq
19
+ from .iq_demodulation import IQDemodulation
20
+ from .least_squares_fit import LeastSquaresFit
21
+ from .synchronous_averaging import SynchronousAveraging
22
+ from .fft_extraction import FFTExtraction
23
+
24
+ __all__ = [
25
+ "ExtractionMethod",
26
+ "VibrationExtractor",
27
+ "detect_rotation_freq",
28
+ "IQDemodulation",
29
+ "LeastSquaresFit",
30
+ "SynchronousAveraging",
31
+ "FFTExtraction",
32
+ ]