### ===================================================================================================================
### Class definition for MeshNode for the vibration simulation
### ===================================================================================================================
# Copyright ©2026 Haskoning Nederland B.V.
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
import numpy as np
from dataclasses import dataclass
from typing import List, Dict
# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.vibration_contour_plot.helper_functions import fem_compare_coordinates, v_to_db, db_to_v
from haskoning_atr_tools.signal_processing import FrequencyDomainData, TimeDomainData
### ===================================================================================================================
### 2. MeshNode class
### ===================================================================================================================
[docs]
@dataclass
class MeshNode:
def __init__(self, id: int, coordinates: List[float]):
self.id = id
self.coordinates = coordinates
self.x = self.coordinates[0]
self.y = self.coordinates[1]
self.z = self.coordinates[2] if self.coordinates[2] else 0
self.frequency_domain_data = None
self.time_domain_data = None
self._vibration_level: Dict = {}
self._vibration_level_unit: str = 'velocity'
def __eq__(self, other, precision: int = 6):
"""Overrides the default implementation"""
if isinstance(other, MeshNode):
return self.id == other.id and fem_compare_coordinates(self.coordinates, other.coordinates)
return False
@property
def project(self):
return self.__project
@project.setter
def project(self, new_project):
self.__project = new_project
@property
def id(self):
return self.__id
@id.setter
def id(self, new_id: int):
if new_id and not isinstance(new_id, int):
raise ValueError(
f"ERROR: The mesh_element requires an integer for id input argument, provided: {new_id}.")
elif new_id and new_id < 1:
raise ValueError(
f"ERROR: The meshnode requires a positive integer, minimum 1 for id input argument, provided: "
f"{new_id}.")
self.__id = new_id
@property
def coordinates(self):
return self.__coordinates
@coordinates.setter
def coordinates(self, new_coordinates):
if not isinstance(new_coordinates, list):
raise TypeError(
f"ERROR: Meshnode input for coordinates of {self.__class__.__name__} must be a list.")
for item in new_coordinates:
if not isinstance(item, (float, int)):
raise TypeError(
f"ERROR: Meshnode input for coordinates of {self.__class__.__name__} must be a list of coordinates "
f"as floats or integers, representing the coordinates in x-, y- and z-direction in [m].")
if len(new_coordinates) == 2:
new_coordinates.append(0)
if len(new_coordinates) != 3:
raise ValueError(
f"ERROR: Meshnode input for coordinates of {self.__class__.__name__} '{new_coordinates}' consists of "
f"{len(new_coordinates)} coordinates, 3 should be provided (x, y and z coordinate in [m]).")
self.__coordinates = new_coordinates
@property
def frequency_domain_data(self):
return self.__frequency_domain_data
@frequency_domain_data.setter
def frequency_domain_data(self, new_frequency_domain_data: FrequencyDomainData):
if new_frequency_domain_data and not isinstance(new_frequency_domain_data, FrequencyDomainData):
raise ValueError(
f"ERROR: The provided frequency domain data is not a {type(FrequencyDomainData)}.")
self.__frequency_domain_data = new_frequency_domain_data
@property
def time_domain_data(self):
return self.__time_domain_data
@time_domain_data.setter
def time_domain_data(self, new_time_domain_data: TimeDomainData):
if new_time_domain_data and not isinstance(new_time_domain_data, TimeDomainData):
raise ValueError(
f"ERROR: The provided frequency domain data is not a {type(TimeDomainData)}.")
self.__time_domain_data = new_time_domain_data
@property
def vibration_level(self):
return self._vibration_level
@property
def vibration_level_unit(self):
return self._vibration_level_unit
@vibration_level_unit.setter
def vibration_level_unit(self, new_unit: str):
if not isinstance(new_unit, str):
raise TypeError("vibration_level_unit must be a string.")
self._vibration_level_unit = new_unit
def calculate_total_level_per_frequency(self, amplitude_type="velocity"):
# Square Root of Sum of Squares should be done based on n velocity
# 1. Initialize a dictionary to hold the SUM OF SQUARES for each frequency
sum_of_squares_per_frequency = dict()
for source_name, source_level_dict in self._vibration_level.items():
for freq, level in source_level_dict.items():
if self.vibration_level_unit == "decibel":
level = db_to_v(level)
# 2. Square the level before adding it to the sum
level_squared = level ** 2
if freq not in sum_of_squares_per_frequency:
sum_of_squares_per_frequency[freq] = level_squared
else:
# Add the squared level to the running sum for this frequency
sum_of_squares_per_frequency[freq] += level_squared
# 3. Calculate the final total level by taking the square root of the sums
total_level_per_frequency = dict()
for freq, sum_sq in sum_of_squares_per_frequency.items():
# Use math.sqrt() for the square root calculation
total_level_per_frequency[freq] = np.sqrt(sum_sq)
if amplitude_type == "decibel":
total_level_per_frequency[freq] = v_to_db(total_level_per_frequency[freq])
return total_level_per_frequency
### ===================================================================================================================
### 3. End of script
### ===================================================================================================================