Source code for haskoning_atr_tools.vibration_contour_plot.simulation.vibration_simulation

### ===================================================================================================================
###  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 ### ===================================================================================================================