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 +69 -0
- pyPRB/__version__.py +5 -0
- pyPRB/core.py +486 -0
- pyPRB/processing/__init__.py +32 -0
- pyPRB/processing/_vibration_extractor.py +339 -0
- pyPRB/processing/fft_extraction.py +230 -0
- pyPRB/processing/iq_demodulation.py +263 -0
- pyPRB/processing/least_squares_fit.py +210 -0
- pyPRB/processing/synchronous_averaging.py +321 -0
- pyprb-1.0.0.dist-info/METADATA +215 -0
- pyprb-1.0.0.dist-info/RECORD +13 -0
- pyprb-1.0.0.dist-info/WHEEL +4 -0
- pyprb-1.0.0.dist-info/licenses/License +21 -0
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
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
|
+
]
|