### ===================================================================================================================
### Class definition for Mesh for the vibration simulation
### ===================================================================================================================
# Copyright ©2026 Haskoning Nederland B.V.
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
import os
import numpy as np
import matplotlib.pyplot as plt
from dataclasses import dataclass
from typing import List, Union, Optional, Tuple
# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.vibration_contour_plot.mesh.mesh_node import MeshNode
from haskoning_atr_tools.vibration_contour_plot.mesh.mesh_element import MeshElement
from haskoning_atr_tools.vibration_contour_plot.helper_functions import fem_compare_coordinates
### ===================================================================================================================
### 2. Mesh class
### ===================================================================================================================
[docs]
@dataclass
class Mesh:
def __init__(self, mesh_elements: List['MeshElement']):
self.mesh_elements = mesh_elements
@property
def project(self):
return self.__project
@project.setter
def project(self, new_project):
self.__project = new_project
@property
def mesh_elements(self):
return self.__mesh_elements
@mesh_elements.setter
def mesh_elements(self, new_mesh_elements):
if not isinstance(new_mesh_elements, list) or \
not all(isinstance(element, MeshElement) for element in new_mesh_elements):
raise ValueError("ERROR: Input for mesh nodes should be a list of MeshElement objects.")
else:
self.__mesh_elements = new_mesh_elements
[docs]
def dimensions(self) -> List[Tuple[float, float]]:
""" Method of 'Mesh' to collect the bounding box of the mesh."""
nodes = np.array([elem.mesh_nodes for elem in self.mesh_elements]).ravel()
coords = np.array([n.coordinates for n in nodes], dtype=float)
mins = coords.min(axis=0)
maxs = coords.max(axis=0)
return [(a, b) for a, b in zip(mins, maxs)]
[docs]
def get_all_mesh_nodes(self) -> List[MeshNode]:
"""Returns a list of all unique MeshNode objects in the mesh."""
unique_nodes = {}
for element in self.mesh_elements:
for node in element.mesh_nodes:
if node.id not in unique_nodes:
unique_nodes[node.id] = node
return list(unique_nodes.values())
[docs]
def plot_mesh(self, figure_size: Union[list, tuple] = (10, 10), file_name: str = "mesh_plot.png",
line_segments=None, constraint_points: Optional[List[Tuple[float, float, float]]] = None):
"""Plots the mesh elements."""
plt.figure(figsize=figure_size)
# Process input for plotting (convert inner lists to tuples for consistency)
processed_line_segments = []
if line_segments:
for seg in line_segments:
processed_line_segments.append(tuple(tuple(pt) for pt in seg))
if constraint_points:
processed_constraint_points = [tuple(pt) for pt in constraint_points]
# Plot triangles
for mesh_element in self.mesh_elements:
pts = []
for node in mesh_element.mesh_nodes:
pts.append([node.x, node.y])
polygon = plt.Polygon(pts, edgecolor='gray', facecolor='none', linewidth=0.5)
plt.gca().add_patch(polygon)
# Plot line segments if provided (using processed_line_segments)
if processed_line_segments:
for seg_tuple in processed_line_segments: # seg_tuple is ((x1, y1), (x2, y2))
x_vals = [seg_tuple[0][0], seg_tuple[1][0]]
y_vals = [seg_tuple[0][1], seg_tuple[1][1]]
plt.plot(x_vals, y_vals, color='red', linewidth=2, label='Constrained Segment')
# Plot constraint points if provided
if processed_constraint_points:
# Extract x and y coordinates for plotting
x_constraint_coords = [pt[0] for pt in processed_constraint_points]
y_constraint_coords = [pt[1] for pt in processed_constraint_points]
plt.scatter(x_constraint_coords, y_constraint_coords, s=50, color='red', marker='X',
label='Constraint Points', zorder=999) # zorder to ensure it's on top
# Plot mesh nodes
x_coords = [node.x for node in self.get_all_mesh_nodes()]
y_coords = [node.y for node in self.get_all_mesh_nodes()]
plt.scatter(x_coords, y_coords, s=8, color='blue', label='Mesh Nodes')
# Avoid duplicate legend entries
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys(), loc='upper right')
plt.title('Constrained Mesh Visualisation')
plt.xlabel('X-coordinate (m)')
plt.ylabel('Y-coordinate (m)')
plt.grid(True)
plt.axis('equal')
plt.tight_layout()
plt.savefig(os.path.join(self.project.results_folder, file_name))
print(f"Mesh plot saved as '{file_name}' in folder: {self.project.results_folder}")
plt.close()
def get_mesh_node_by_coordinates(self, coordinates: List[float]) -> Optional['MeshNode']:
found = False
for node in self.get_all_mesh_nodes():
if fem_compare_coordinates(node.coordinates, coordinates):
return node
raise ValueError(f"ERROR: No mesh node found with coordinates {coordinates}.")
def get_mesh_node_by_coordinates_2d(self, coordinates: List[float]) -> Optional['MeshNode']:
# Ensure the input coordinates list has at least two elements (x and y)
if len(coordinates) < 2:
raise ValueError("Input coordinates must contain at least x and y values.")
# Only compare the first two (x and y) coordinates of the input
target_xy = coordinates[:2]
for node in self.get_all_mesh_nodes():
node_coords = node.coordinates
# Ensure node coordinates have at least two elements
if len(node_coords) < 2:
continue # Skip nodes that don't have enough coordinates
# Get only the first two (x and y) coordinates of the mesh node
node_xy = node_coords[:2]
# Use your custom comparison function, passing only the X and Y slices
if fem_compare_coordinates(node_xy, target_xy):
return node
# # The 'found' flag is removed, as raising the error handles the not found case.
# raise ValueError(f"ERROR: No mesh node found with XY coordinates {target_xy}.")
### ===================================================================================================================
### 3. End of script
### ===================================================================================================================