### ===================================================================================================================
### Class for the vibration simulation
### ===================================================================================================================
# Copyright ©2026 Haskoning Nederland B.V.
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
import numpy as np
from warnings import warn
from typing import List, Literal, Optional
# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.vibration_contour_plot.helper_functions import v_to_db, db_to_v
from haskoning_atr_tools.vibration_contour_plot.config import ATRConfigVibrationContourPlot as Config
from haskoning_atr_tools.vibration_contour_plot.mesh import MeshNode
from haskoning_atr_tools.vibration_contour_plot.equipment import Equipment
from haskoning_atr_tools.vibration_contour_plot.simulation.material import Material
### ===================================================================================================================
### 2. VibrationSource class
### ===================================================================================================================
[docs]
class VibrationSource:
""" Instances of the VibrationSource class represent a single vibration source."""
def __init__(
self, id: int, mesh_node: MeshNode, equipment: Optional[Equipment] = None, z: float = 0):
"""
Input:
- id (int): Identifier of the vibration source
- mesh_node (MeshNode): The mesh node at the position of the vibration source.
- equipment (Equipment): Optional input for the instance of the Equipment. Default value is None.
- z (float): Vertical distance of the vibration source to the reference level, in [m]. Default value is 0.
"""
warn(
"WARNING: The Vibration Contour Plot Tool is currently under active development. Functionality may change "
"in future updates, and formal validation is still pending. Please verify your results carefully before "
"using them in your applications.")
self.id = id
self.mesh_node = mesh_node
self.equipment = equipment
self.z = z
self.simulation = None
@property
def simulation(self):
return self.__simulation
@simulation.setter
def simulation(self, new_simulation):
self.__simulation = new_simulation
@property
def mesh_node(self):
return self.__mesh_node
@mesh_node.setter
def mesh_node(self, new_mesh_node: MeshNode):
if not isinstance(new_mesh_node, MeshNode):
raise TypeError(
f"ERROR: Input for mesh node should be an instance of MeshNode class. Provided was "
f"{type(new_mesh_node)}.")
self.__mesh_node = new_mesh_node
@property
def equipment(self):
return self.__equipment
@equipment.setter
def equipment(self, new_equipment: Equipment):
if not isinstance(new_equipment, Equipment):
raise TypeError(
f"ERROR: Input for equipment should be an instance of Equipment class. Provided was "
f"{type(new_equipment)}.")
self.__equipment = new_equipment
[docs]
def get_vibration_level_at_source(self):
""" Method of 'VibrationSource' to collect the frequency data for the attenuation model, and ensure values are
in velocity."""
vibration_level_at_source = dict()
if self.equipment.frequency_domain_data is None:
raise ValueError(
"ERROR: Frequency domain data is not defined for this source. Please create the frequency spectra "
"first.")
if self.equipment.frequency_domain_data.amplitude_units == 'dB':
for freq, amp in zip(
self.equipment.frequency_domain_data.frequencies, self.equipment.frequency_domain_data.amplitudes):
vibration_level_at_source[freq] = db_to_v(amp)
elif self.equipment.frequency_domain_data.amplitude_units == 'm/s':
for freq, amp in zip(
self.equipment.frequency_domain_data.frequencies, self.equipment.frequency_domain_data.amplitudes):
vibration_level_at_source[freq] = amp
else:
raise RuntimeError(
f"ERROR: Unexpected frequency domain amplitude unit encountered: "
f"{self.equipment.frequency_domain_data.amplitude_units}. Select from 'm/s' or 'dB'.")
return vibration_level_at_source
[docs]
def calculate_level_at_point(
self, target_point: MeshNode, amplitude_type: Literal['velocity', 'decibel'] = 'velocity'):
"""
Calculates the target vibration velocity level per frequency at the requested point due to this single vibration
source.
Input:
- target_point (MeshNode): The target point.
- material (Material): The material properties to be used in the attenuation model.
- amplitude_type (str): The type of amplitude of the vibration level. Select from 'decibel' or 'velocity'.
Default value is 'velocity'.
Returns:
- Returns the vibration level at the requested point for this single vibration source.
"""
# Material parameters for attenuation
rho_B = self.simulation.material.barkan_material_parameter
# Source distance to source (should be zero, but set to a small value to avoid division by zero)
# Set in configuration file
r_a = Config.MINIMUM_SOURCE_DISTANCE
# Target distance to source
r_b = np.sqrt(
(target_point.x - self.mesh_node.x) ** 2 + (target_point.y - self.mesh_node.y) ** 2 +
(target_point.z - self.mesh_node.z) ** 2)
# Calculation below is based on level_at_source_dict to be velocity
# The distance attenuation formula is applied here.
if r_b < r_a:
# If the point is at the source, return the initial level in velocity.
level_at_target_dict = self.get_vibration_level_at_source()
else:
# Calculate the level at the target always in velocity
level_at_target_dict = dict()
level_at_source_dict = self.get_vibration_level_at_source()
delta_r = r_a - r_b
geometrical_factor = (r_a / r_b) ** self.simulation.gamma
for freq, vl_a in level_at_source_dict.items():
# Attenuation model: v_b/v_a = (r_a/r_b)^gamma * e^(rho_B*Pi*f*(r_a-r_b))
# in which v_b (Target) and v_a (Source) are in velocity
vb_to_va_ratio = geometrical_factor * np.exp(rho_B * np.pi * freq * delta_r)
level_at_target_dict[freq] = vl_a * vb_to_va_ratio
# Convert to decibel if needed
if amplitude_type == 'decibel':
for freq, velocity in level_at_target_dict.items():
level_at_target_dict[freq] = v_to_db(velocity)
return level_at_target_dict
### ===================================================================================================================
### 3. VibrationSimulation class
### ===================================================================================================================
[docs]
class VibrationSimulation:
"""
The instance of the VibrationSimulation manages a collection of vibration sources and calculates the total vibration
field.
"""
def __init__(
self, name: str, material: Material, vibration_sources: List[VibrationSource], gamma: float = Config.GAMMA):
"""
Input:
- name (str): The name of the vibration simulation.
- material (Material): Material containing the material properties for the vibration simulation.
"""
self.project = None
self.name = name
self.material = material
self.gamma = gamma
self.vibration_sources = vibration_sources
@property
def project(self):
return self.__project
@project.setter
def project(self, new_project):
self.__project = new_project
@property
def name(self):
return self.__name
@name.setter
def name(self, new_name):
self.__name = new_name
@property
def material(self):
return self.__material
@material.setter
def material(self, new_material: Material):
if not isinstance(new_material, Material):
raise TypeError(
f"ERROR: Input for the material should be an instance of Material class. Provided was "
f"{type(new_material)}.")
self.__material = new_material
@property
def gamma(self):
return self.__gamma
@gamma.setter
def gamma(self, new_gamma: float = Config.GAMMA):
if not isinstance(new_gamma, (float, int)):
raise TypeError(f"ERROR: Input for the gamma should be a float. Provided was {type(new_gamma)}.")
if new_gamma <= 0:
raise ValueError(f"ERROR: Input for the gamma {new_gamma} should be larger than zero.")
self.__gamma = new_gamma
@property
def vibration_sources(self) -> List[VibrationSource]:
return self.__vibration_sources
@vibration_sources.setter
def vibration_sources(self, new_vibration_sources: List[VibrationSource]):
if not isinstance(new_vibration_sources, list):
new_vibration_sources = [new_vibration_sources]
if not all(isinstance(source, VibrationSource) for source in new_vibration_sources):
raise TypeError(
f"ERROR: Input for vibration sources should be a list of VibrationSource objects. Provided was "
f"{type(new_vibration_sources)}.")
self.__vibration_sources = new_vibration_sources
for vibration_source in self.vibration_sources:
vibration_source.simulation = self
[docs]
def add_vibration_source(self, vibration_source: VibrationSource):
""" Method of 'VibrationSimulation' adds a single VibrationSource object to the simulation."""
self.vibration_sources = self.vibration_sources.append(vibration_source)
[docs]
def calculate_level_per_node_per_source(
self, amplitude_type: Literal['velocity', 'decibel'] = 'velocity',
vibration_source: Optional[VibrationSource] = None):
""" Method of 'VibrationSimulation' calculates the total vibration level at a given point by summing the
contributions from all sources."""
if not self.vibration_sources:
return None
if vibration_source:
for mesh_node in self.project.mesh.get_all_mesh_nodes():
level_from_source_dict = vibration_source.calculate_level_at_point(
target_point=mesh_node, amplitude_type=amplitude_type)
if vibration_source.id not in mesh_node.vibration_level:
mesh_node.vibration_level[vibration_source.id] = level_from_source_dict
mesh_node.vibration_level_unit = amplitude_type
else:
raise ValueError(
f"ERROR: Vibration level from source '{vibration_source.id}' already calculated for node "
f"{mesh_node.id}.")
else:
for mesh_node in self.project.mesh.get_all_mesh_nodes():
for source in self.vibration_sources:
level_from_source_dict = source.calculate_level_at_point(
target_point=mesh_node, amplitude_type=amplitude_type)
if source.id not in mesh_node.vibration_level:
mesh_node.vibration_level[source.id] = level_from_source_dict
mesh_node.vibration_level_unit = amplitude_type
else:
raise ValueError(
f"ERROR: Vibration level from source '{source.id}' already calculated for node "
f"{mesh_node.id}.")
### ===================================================================================================================
### 4. End of script
### ===================================================================================================================