### ===================================================================================================================
### Class for vibration parameters of equipment
### ===================================================================================================================
# Copyright ©2026 Haskoning Nederland B.V.
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
import math
import numpy as np
from warnings import warn
from dataclasses import dataclass, field
from typing import Optional, Literal
# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.signal_processing import FrequencyDomainData
from haskoning_atr_tools.vibration_contour_plot.helper_functions import db_to_v
### ===================================================================================================================
### 2. Equipment class
### ===================================================================================================================
[docs]
@dataclass(frozen=True)
class Equipment:
"""
Instances of this class represent a piece of equipment that generates vibrations for vibration analysis purposes.
Input:
- name (str): The name of the equipment.
- id (int): The identifier of the equipment.
- coordinates (list of float): The coordinates (e.g., [x, y, z]) of the equipment's location, values in [m].
- vibration_source_level (float): The source vibration level emitted by the equipment, in [dB].
- min_frequency (float): The minimum frequency of the vibration range the equipment produces, in [Hz].
- max_frequency (float): The maximum frequency of the vibration range the equipment produces, in [Hz].
- flat_noise_percent (float): Percentage of flat noise added to the vibration signal, in [%].
- activity (str): Description of the activity or operational state of the equipment. Select from 'continuous',
'intermittent' or 'emergency'.
- rpm, center_frequency, angular_frequency (float): Optional input. Provide at least one of these to compute the
others.
"""
name: str
id: int
coordinates: tuple[float, float, float]
vibration_source_level: float
min_frequency: float
max_frequency: float
flat_noise_percent: float
activity: Literal['continuous', 'intermittent', 'emergency']
# Internal fields (computed if not provided)
_rpm: Optional[float] = field(default=None)
_center_frequency: Optional[float] = field(default=None)
_angular_frequency: Optional[float] = field(default=None)
# Store calculated values
_frequency_domain_data: FrequencyDomainData = field(default=None)
def __post_init__(self):
""" Validate and calculate interdependent values after initialisation."""
# Validations
if len(self.coordinates) != 3:
raise ValueError(
f"ERROR: Coordinates must be a tuple of three values [x, y, z]. Provided is {self.coordinates} for "
f"{self.name}.")
if self.min_frequency <= 0 or self.max_frequency <= 0:
raise ValueError(
f"ERROR: Frequencies must be positive. Provided are minimum frequency {self.min_frequency}Hz and "
f"maximum {self.max_frequency}Hz for {self.name}.")
if not (0 <= self.flat_noise_percent <= 100):
raise ValueError(
f"ERROR: The flat noise percentage must be between 0 and 100. Provided is {self.flat_noise_percent}% "
f"for {self.name}.")
if self.activity not in {'continuous', 'intermittent', 'emergency'}:
raise ValueError(
f"ERROR: Activity must be 'continuous', 'intermittent', or 'emergency'. Provided is {self.activity} "
f"for {self.name}.")
if self.min_frequency >= self.max_frequency:
raise ValueError(
f"ERROR: The minimum frequency {self.min_frequency}Hz must be less than the maximum frequency "
f"{self.max_frequency}Hz for {self.name}.")
if not (1 <= self.vibration_source_level <= 160):
warn(f"WARNING: The source vibration level is not within expected range for {self.name}. Please assure the "
f"value {self.vibration_source_level}dB is correct. The tool will continue to use this value.")
# Compute missing frequency values
rpm = self._rpm
cf = self._center_frequency
af = self._angular_frequency
if rpm:
cf = cf or rpm / 60
af = af or 2 * math.pi * rpm / 60
elif cf:
rpm = rpm or cf * 60
af = af or 2 * math.pi * cf
elif af:
cf = cf or af / (2 * math.pi)
rpm = rpm or cf * 60
else:
raise ValueError(
f"ERROR: Provide at least one of rpm, center_frequency, or angular_frequency for {self.name}.")
object.__setattr__(self, '_rpm', rpm)
object.__setattr__(self, '_center_frequency', cf)
object.__setattr__(self, '_angular_frequency', af)
# Validate the center frequency
if not (self.min_frequency <= self.center_frequency <= self.max_frequency):
raise ValueError(
f"ERROR: The center frequency {self.center_frequency} (rpm={self.rpm}) is not within the provided "
f"range with minimum frequency {self.min_frequency}Hz and maximum {self.max_frequency}Hz for "
f"{self.name}.")
def __repr__(self) -> str:
return \
f"Equipment '{self.name}' ({self.activity}): Freq range {self.min_frequency}-{self.max_frequency} Hz, " \
f"Center freq {self.center_frequency:.2f} Hz, RPM {self.rpm:.2f}"
@property
def rpm(self) -> float:
""" Returns the value of the rounds per minute of the equipment vibration, in [1/min]."""
return self._rpm
@property
def center_frequency(self) -> float:
""" Returns the value of the center frequency of the equipment vibration, in [Hz]."""
return self._center_frequency
@property
def angular_frequency(self) -> float:
""" Returns the value of the angular frequency of the equipment vibration, in [rad/s]."""
return self._angular_frequency
@property
def frequency_domain_data(self) -> Optional[FrequencyDomainData]:
""" Returns the frequency spectrum of the equipment vibration, in [1/min]."""
return self._frequency_domain_data
@property
def vibration_source_level_in_v(self) -> float:
""" Returns the vibration source level as velocity, in [m/s]."""
return db_to_v(self.vibration_source_level)
[docs]
def create_frequency_domain_data(
self, project, sampling_rate: float, lowest_frequency: float = 0.0) -> FrequencyDomainData:
"""
Method of 'Equipment' to create frequency domain data for this equipment. The frequency domain data contains the
combination of the harmonic component and the white noise component. The white noise component is not included
in the part where the harmonic component is defined.
Input:
- project (obj): Project object containing collections of objects and project variables.
- sampling_rate (float): Frequency sampling rate, in [Hz].
- lowest_frequency (float): Lower frequency bound, in [Hz]. Default value is `1Hz.
Output:
- The harmonic, white noise and combined frequency domain data is created for this equipment and added to
the respective lists in project, 'fdd_harmonic_collection', 'fdd_noise_collection' and
'fdd_sources_collection'.
- Returns the combined frequency domain data.
"""
# Create frequency list based on the provided sampling rate
frequencies = np.arange(self.min_frequency, self.max_frequency, sampling_rate).tolist()
# Create the harmonic frequency spectrum and add to the 'fdd_harmonic_collection' collection of project
harmonic_frequency_domain_data = project.create_harmonic_spectrum_frequency_domain_data(
name=f'{self.name}_h_data', amplitudes=[self.vibration_source_level_in_v for _ in frequencies],
frequencies=frequencies, phase_angles=[n * 0 for n in frequencies], lowest_frequency=lowest_frequency,
# highest_frequency=2 * self.max_frequency,
highest_frequency=100,
sampling_rate=sampling_rate, amplitude_type='harmonic',
amplitude_units='m/s', frequency_units='Hz', collection_name='fdd_harmonic_collection')
# Create the white noise frequency spectrum and add to the 'fdd_noise_collection' collection of project
white_noise_frequency_domain_data = project.create_white_noise_spectrum_frequency_domain_data(
name=f'{self.name}_n_data',
noise_amplitude=self.flat_noise_percent * self.vibration_source_level_in_v / 100,
frequencies=harmonic_frequency_domain_data.frequencies, amplitude_units='m/s', frequency_units='Hz',
collection_name='fdd_noise_collection')
# Combine the amplitudes for the full frequency domain, exclude noise in the part where the harmonic frequencies
# are defined.
fdd_amp = []
for i in range(len(harmonic_frequency_domain_data.frequencies)):
if harmonic_frequency_domain_data.frequencies[i] not in frequencies:
fdd_amp.append(
white_noise_frequency_domain_data.amplitudes[i] + harmonic_frequency_domain_data.amplitudes[i])
else:
fdd_amp.append(harmonic_frequency_domain_data.amplitudes[i])
# Create the combined frequency spectrum and add to the 'fdd_sources_collection' collection of project
fdd = project.create_frequency_domain_data(
name=f'{self.name}', frequencies=harmonic_frequency_domain_data.frequencies, values=fdd_amp,
phase_angles=harmonic_frequency_domain_data.phase_angles, amplitude_type='velocity', value_units='m/s',
spectrum_type='amplitude', frequency_units='Hz', collection_name='fdd_sources_collection')
object.__setattr__(self, '_frequency_domain_data', fdd)
return fdd
### ===================================================================================================================
### 3. End of script
### ===================================================================================================================