### ===================================================================================================================
### Plot functions for Vibration Contour Plot
### ===================================================================================================================
# Copyright ©2026 Haskoning Nederland B.V.
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
import numpy as np
from pathlib import Path
from typing import Optional, Union, Literal, List, Tuple
# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.signal_processing.utils.plot import plot_function
from haskoning_atr_tools.vibration_contour_plot.helper_functions import db_to_v, v_to_db
from haskoning_atr_tools.vibration_contour_plot.mesh import MeshNode
from haskoning_atr_tools.vibration_contour_plot.simulation import VibrationSimulation
from haskoning_atr_tools.vibration_contour_plot.plot.contour_plot import ContourPlot
### ===================================================================================================================
### 2. Create contour plots
### ===================================================================================================================
[docs]
def plot_contour(
project, at_frequencies: List[float], vibration_simulation: VibrationSimulation,
figure_size: Tuple[float, float] = (10, 8), line_segments: Optional[List[List[List[float]]]] = None,
amplitude_type: Literal['velocity', 'decibel'] = 'decibel', n_levels: int = 10, align_colour_bar: bool = False,
show_mesh: bool = True, min_limit: Optional[float] = None, max_limit: Optional[float] = None,
sources_colour: str = 'blue', targets_colour: str = 'blue', plot_origin: bool = True, label_font_size: int = 10,
colour_map_type: Literal['continuous', 'discrete'] = 'discrete', colour_map: Optional = None,
show: bool = False) -> List[Path]:
"""
Function to create per-frequency filled contour plots of total vibration level across the mesh.
Input:
- project (obj): Project object containing collections of objects and project variables.
- at_frequencies (list of float): Frequencies, in [Hz], at which to render individual contour plots.
- vibration_simulation (VibrationSimulation): Simulation object.
- figure_size (tuple of two floats): Matplotlib figure size in inches (width, height). Default value is (10, 8).
- line_segments (list of list of list of float): Optional input for additional lines to be drawn in red.
- amplitude_type (str): Amplitude domain, select from 'velocity' or 'decibel'. Used for total level per
frequency. Default value is 'decibel'.
- n_levels (int): Number of contour levels in the plot. Default value 10.
- align_colour_bar (bool): Select to share a common min/max (computed across requested frequencies). Default
value is False.
- show_mesh (bool): Draw mesh triangles as light wireframe overlays. Default value is True.
- min_limit, max_limit (float): Lower and upper bound used when `align_color_bar=True` (floor / cap). Default
value is None, minimum and maximum are based on the data.
- sources_colour (str): Optional input for the colour of the source markers (if provided). Default value is
'blue'.
- targets_colour (str): Optional input for the colour of the target markers (if provided). Default value is
'blue'.
- plot_origin (bool): Select to draw a marker at the origin. Default value is True.
- label_font_size (int): Font size for labels (sources, targets and origin). Default value is 10.
- colour_map_type (str): Select from 'continuous' or 'discrete' for the type of colour map to be used in the
contour plot. Default value is 'discrete'.
- colour_map (matplotlib colormap): If provided, overrides the internally constructed colour map. Default
value is None.
- show (bool): Select to display the figure window in GUI. Default value is False.
Output:
- Creates contour plots and returns list of saved file paths (png) for each requested frequency. Files are saved
in the results folder specified for the project.
"""
# Collect the mesh node data
all_mesh_nodes = project.mesh.get_all_mesh_nodes()
all_mesh_nodes.sort(key=lambda n: n.id)
all_triangle_elements = [element.get_mesh_nodes_ids() for element in project.mesh.mesh_elements]
# Level per mesh-node
total_level_per_frequency = {
node.id: node.calculate_total_level_per_frequency(amplitude_type=amplitude_type) for node in all_mesh_nodes}
# Create ContourPlot instances for the requested frequencies
contour_plots = [
ContourPlot(
file_name=f'{freq}Hz', mesh=project.mesh, figure_size=figure_size,
values=[total_level_per_frequency[node.id][freq] for node in all_mesh_nodes]) for freq in at_frequencies]
# Add contour plots to project
for contour_plot in contour_plots:
contour_plot.project = project
# Create plots
created_plots = []
if isinstance(min_limit, (float, int)):
min_val = min_limit
else:
min_val = min([np.min(cp.values).item() for cp in contour_plots]) if align_colour_bar else None
if isinstance(max_limit, (float, int)):
max_val = max_limit
else:
max_val = max([np.max(cp.values) for cp in contour_plots]) if align_colour_bar else None
for contour_plot, at_frequency in zip(contour_plots, at_frequencies):
_, file_path = contour_plot.plot(
nodes=all_mesh_nodes, triangles=all_triangle_elements, n_levels=n_levels,
line_segments=line_segments, vibration_sources=vibration_simulation.vibration_sources,
sources_colour=sources_colour, targets=project.target_list, targets_colour=targets_colour,
plot_origin=plot_origin, show_mesh=show_mesh, label_font_size=label_font_size,
colour_map_type=colour_map_type, min_val=min_val, max_val=max_val, colour_map=colour_map, show=show,
file_path=True)
created_plots.append(file_path)
return created_plots
[docs]
def plot_governing_contour(
project, vibration_simulation: VibrationSimulation, figure_size: Tuple[float, float] = (10, 8),
line_segments: Optional[List[List[List[float]]]] = None,
amplitude_type: Literal['velocity', 'decibel'] = 'decibel', n_levels: int = 10, show_mesh: bool = True,
min_limit: Optional[float] = None, max_limit: Optional[float] = None, sources_colour: str = 'blue',
targets_colour: str = 'blue', plot_origin: bool = True, label_font_size: int = 10,
colour_map_type: Literal['continuous', 'discrete'] = 'discrete', colour_map: Optional = None,
show: bool = False, use_plotly: bool = False) -> Optional[Path]:
"""
Function to create single governing contour plot.
Input:
- project (obj): Project object containing collections of objects and project variables.
- vibration_simulation (VibrationSimulation): Simulation object.
- figure_size (tuple of two floats): Matplotlib figure size in inches (width, height). Default value is (10, 8).
- line_segments (list of list of list of float): Optional input for additional lines to be drawn in red.
- amplitude_type (str): Amplitude domain, select from 'velocity' or 'decibel'. Used for total level per
frequency. Default value is 'decibel'.
- n_levels (int): Number of contour levels in the plot. Default value 10.
- show_mesh (bool): Draw mesh triangles as light wireframe overlays. Default value is True.
- min_limit, max_limit (float): Lower and upper bound used when `align_color_bar=True` (floor / cap). Default
value is None, minimum and maximum are based on the data.
- sources_colour (str): Optional input for the colour of the source markers (if provided). Default value is
'blue'.
- targets_colour (str): Optional input for the colour of the target markers (if provided). Default value is
'blue'.
- plot_origin (bool): Select to draw a marker at the origin. Default value is True.
- label_font_size (int): Font size for labels (sources, targets and origin). Default value is 10.
- colour_map_type (str): Select from 'continuous' or 'discrete' for the type of colour map to be used in the
contour plot. Default value is 'discrete'.
- colour_map (matplotlib colormap): If provided, overrides the internally constructed colour map. Default
value is None.
- show (bool): Select to display the figure window in GUI. Default value is False.
- use_plotly (bool): Select to use plotly to generate the image. Default value is False, using matplotlib.
Plotly is used for interactive plotting.
Output:
- Creates governing contour plot and returns file path of the created png-file. File is saved in the results
folder specified for the project.
"""
# collect the mesh node data
all_mesh_nodes = project.mesh.get_all_mesh_nodes()
all_mesh_nodes.sort(key=lambda n: n.id)
all_triangle_elements = [element.get_mesh_nodes_ids() for element in project.mesh.mesh_elements]
# Level per mesh-node and add total
total_level_per_frequency = {
node.id: node.calculate_total_level_per_frequency(amplitude_type=amplitude_type) for node in all_mesh_nodes}
total_level_max = dict()
for node in all_mesh_nodes:
total_levels = total_level_per_frequency[node.id]
max_level_freq = max(total_levels, key=total_levels.get)
max_level = total_levels[max_level_freq]
total_level_max[node.id] = {max_level_freq: max_level}
# Create contour plot instance
z = [list(total_level_max[node.id].values())[0] for node in all_mesh_nodes]
cp = ContourPlot(file_name=f'Governing vibration', mesh=project.mesh, figure_size=figure_size, values=z)
cp.project = project
# Create plot
if isinstance(min_limit, (float, int)):
min_val = min_limit
else:
min_val = np.min(cp.values).item()
if isinstance(max_limit, (float, int)):
max_val = max_limit
else:
max_val = np.max(cp.values).item()
if use_plotly:
cp.plot_plotly(
nodes=all_mesh_nodes, n_levels=n_levels, line_segments=line_segments,
vibration_sources=vibration_simulation.vibration_sources, min_val=min_val, max_val=max_val)
file_path = None
else:
_, file_path = cp.plot(
nodes=all_mesh_nodes, triangles=all_triangle_elements, n_levels=n_levels,
line_segments=line_segments, vibration_sources=vibration_simulation.vibration_sources,
sources_colour=sources_colour, targets=project.target_list, targets_colour=targets_colour,
plot_origin=plot_origin, show_mesh=show_mesh, label_font_size=label_font_size,
colour_map_type=colour_map_type, min_val=min_val, max_val=max_val, colour_map=colour_map, show=show,
file_path=True, governing_total_level_dict = total_level_max,)
return file_path
### ===================================================================================================================
### 3. Create spectrum plots
### ===================================================================================================================
[docs]
def plot_f_spectrum_at_targets(
project, amplitude_type: Literal['velocity', 'decibel'] = 'velocity', log_scale_x: bool = False,
log_scale_y: bool = False, plot_components: bool = False, show: bool = False, line_width: float = 1):
"""
Plot frequency spectra at each target location, optionally including component curves.
Input:
- project (obj): Project object containing collections of objects and project variables.
- amplitude_type (str): Amplitude domain, select from 'velocity' or 'decibel'. Used for total level per
frequency. Default value is 'velocity'.
- log_scale_x, log_scale_y (bool): Select to apply logarithmic scaling for the x and y-axis. Default value is
False, no logarithmic scaling.
- plot_components (bool): Select to include the spectra of the components in the plot. Default value is False.
- show (bool): Select to display the figure window in GUI. Default value is False.
- line_width (float): Specify the line width used in the plot. Default value is 1.
Output:
- Creates the spectra at each target location and returns the list of file paths of the created png-files.
Files are saved in the results folder specified for the project.
"""
return [
plot_f_spectrum_at_node(
project=project, node=[target.coordinates[0], target.coordinates[1]], amplitude_type=amplitude_type,
log_scale_x=log_scale_x, log_scale_y=log_scale_y, plot_components=plot_components, mep_limits=None,
target_name=target.name, show=show, line_width=line_width) for target in project.target_list]
[docs]
def plot_f_spectrum_all_targets(
project, amplitude_type: Literal['decibel', 'velocity'] = 'velocity', log_scale_x: bool = False,
log_scale_y: Optional[bool] = None, show: bool = False, line_width: float = 1, compliance_limits: bool = False)\
-> Union[Path, List[Path]]:
"""
Plot frequency spectrum with all targets in one graph, optionally including compliance limits.
Input:
- project (obj): Project object containing collections of objects and project variables.
- amplitude_type (str): Amplitude domain, select from 'velocity' or 'decibel'. Used for total level per
frequency. Default value is 'velocity'.
- log_scale_x, log_scale_y (bool): Select to apply logarithmic scaling for the x and y-axis. Default value is
False, no logarithmic scaling.
- show (bool): Select to display the figure window in GUI. Default value is False.
- line_width (float): Specify the line width used in the plot. Default value is 1.
Output:
- Creates the spectrum of all target locations for all compliance limits and returns the file paths of the
created png-files. Files are saved in the results folder specified for the project.
"""
if not compliance_limits:
nodes = []
target_names = []
for target in project.target_list:
nodes.append([target.coordinates[0], target.coordinates[1]])
target_names.append(target.name)
return plot_f_spectrum_at_multiple_nodes(
project=project, nodes=nodes, target_names=target_names, target_type=None,
mep_limits=None, amplitude_type=amplitude_type, log_scale_x=log_scale_x, log_scale_y=log_scale_y,
show=show, line_width=line_width)
all_mep_limits = dict()
for target in project.target_list:
if target.limits is None:
continue
elif amplitude_type in target.limits:
mep_limits = {
'amplitude_type': amplitude_type,
'freq': target.limits['velocity'].frequencies,
f'limit_{target.type}': target.limits[amplitude_type].limits}
elif amplitude_type == 'velocity' and 'decibel' in target.limits:
mep_limits = {
'amplitude_type': amplitude_type,
'freq': target.limits['decibel'].frequencies,
f'limit_{target.type}': [db_to_v(db_level) for db_level in target.limits['decibel'].limits]}
elif amplitude_type == 'decibel' and 'velocity' in target.limits:
mep_limits = {
'amplitude_type': amplitude_type,
'freq': target.limits['velocity'].frequencies,
f'limit_{target.type}': [v_to_db(v_level) for v_level in target.limits['velocity'].limits]}
else:
continue
# Limit the max decibel to 120Vdb, because too large is unreasonable
if amplitude_type == 'decibel':
limits = mep_limits[f'limit_{target.type}']
mep_limits = {
'amplitude_type': amplitude_type,
'freq': target.limits['velocity'].frequencies,
f'limit_{target.type}': [min(i, 120) for i in limits]}
if target.type not in all_mep_limits:
all_mep_limits[target.type] = mep_limits
if log_scale_y is None:
log_scale_y = True if amplitude_type == 'velocity' else False
created_files = []
for target_typ, mep_limits in all_mep_limits.items():
nodes = []
target_names = []
for target in project.target_list:
if target.type == target_typ:
nodes.append([target.coordinates[0], target.coordinates[1]])
target_names.append(target.name)
created_files.append(plot_f_spectrum_at_multiple_nodes(
project=project, nodes=nodes, target_names=target_names, target_type=target_typ,
mep_limits=mep_limits, amplitude_type=amplitude_type, log_scale_x=log_scale_x, log_scale_y=log_scale_y,
show=show, line_width=line_width))
return created_files
[docs]
def plot_f_spectrum_against_limits_at_targets(
project, amplitude_type: Literal['velocity', 'decibel'] = 'decibel', log_scale_x: bool = False,
log_scale_y: Optional[bool] = None, show: bool = False, line_width: float = 1) -> List[Path]:
"""
Plot frequency spectra at each target location, including the compliance limits set for the target.
Input:
- project (obj): Project object containing collections of objects and project variables.
- amplitude_type (str): Amplitude domain, select from 'velocity' or 'decibel'. Used for total level per
frequency. Default value is 'velocity'.
- log_scale_x, log_scale_y (bool): Select to apply logarithmic scaling for the x and y-axis. Default value is
False, no logarithmic scaling.
- show (bool): Select to display the figure window in GUI. Default value is False.
- line_width (float): Specify the line width used in the plot. Default value is 1.
Output:
- Creates the spectra at each target location and returns the list of file paths of the created png-files.
Files are saved in the results folder specified for the project.
"""
created_files = []
for target in project.target_list:
mep_limits = dict()
if target.limits is None:
continue
elif amplitude_type in target.limits:
mep_limits = {
'amplitude_type': amplitude_type,
'freq': target.limits['velocity'].frequencies,
f'limit_{target.type}': target.limits[amplitude_type].limits}
else:
if amplitude_type == 'velocity':
if 'decibel' in target.limits:
mep_limits = {
'amplitude_type': amplitude_type,
'freq': target.limits['decibel'].frequencies,
f'limit_{target.type}': [db_to_v(db_level) for db_level in target.limits['decibel'].limits]}
if amplitude_type == 'decibel':
if 'velocity' in target.limits:
mep_limits = {
'amplitude_type': amplitude_type,
'freq': target.limits['velocity'].frequencies,
f'limit_{target.type}': [v_to_db(v_level) for v_level in target.limits['velocity'].limits]}
# Limit the max decibel to 120 dB, because too large is unreasonable
if amplitude_type == 'decibel':
limits = mep_limits[f'limit_{target.type}']
mep_limits = {
'amplitude_type': amplitude_type,
'freq': target.limits['velocity'].frequencies,
f'limit_{target.type}': [min(i, 120) for i in limits]}
if log_scale_y is None:
log_scale_y = True if amplitude_type == 'velocity' else False
created_files.append(plot_f_spectrum_at_node(
project=project, node=[target.coordinates[0], target.coordinates[1]], amplitude_type=amplitude_type,
log_scale_x=log_scale_x, log_scale_y=log_scale_y, plot_components=False, mep_limits=mep_limits,
target_name=target.name, show=show, line_width=line_width))
return created_files
[docs]
def plot_f_spectrum_at_node(
project, node: Union[MeshNode, list[float]], amplitude_type: Literal['velocity', 'decibel'] = 'velocity',
log_scale_x: bool = False, log_scale_y: bool = False, plot_components: bool = False,
mep_limits: Optional[dict] = None, target_name: Optional[Union[str, int]] = None, show: bool = False,
line_width: float = 1) -> Path:
"""
Plot frequency spectrum at node.
Input:
- project (obj): Project object containing collections of objects and project variables.
- node (MeshNode or list of float): Either a `MeshNode` or coordinates x, y, (z) used to look up the node on the
mesh.
- amplitude_type (str): Amplitude domain, select from 'velocity' or 'decibel'. Used for total level per
frequency. Default value is 'velocity'.
- log_scale_x, log_scale_y (bool): Select to apply logarithmic scaling for the x and y-axis. Default value is
False, no logarithmic scaling.
- plot_components (bool): Select to include the spectra of the components in the plot. Default value is False.
- mep_limits (dict): Specify the compliance limits used in the plot. Default value is None.
- target_name (str or int): Optional input for the name or ID of the target for titles.
- show (bool): Select to display the figure window in GUI. Default value is False.
- line_width (float): Specify the line width used in the plot. Default value is 1.
Output:
- Creates the spectrum at the requested location and returns the file path of the created png-file. File is
saved in the results folder specified for the project.
"""
# Collect the requested mesh-node
if isinstance(node, (list, tuple)) and all(isinstance(coord, (float, int)) for coord in node):
mesh_node = project.mesh.get_mesh_node_by_coordinates_2d(coordinates=node)
else:
mesh_node = node
if not isinstance(mesh_node, MeshNode):
raise ValueError("ERROR: Node not found, node must be a list of coordinates [x, y, (z)] or a MeshNode object.")
x_values = []
y_values = []
labels = []
# Total level per frequency
total_level_per_frequency = mesh_node.calculate_total_level_per_frequency(amplitude_type=amplitude_type)
x_values.append(list(total_level_per_frequency.keys()))
y_values.append([total_level_per_frequency[f] for f in total_level_per_frequency])
labels.append(f'Total level at {int(node[0]), int(node[1])}')
if plot_components:
for comp_name, data in mesh_node.vibration_level.items():
labels.append(str(comp_name))
x_values.append(list(data.keys()))
if mesh_node.vibration_level_unit == amplitude_type:
y_values.append([data[f] for f in data])
elif mesh_node.vibration_level_unit == 'velocity' and amplitude_type == 'decibel':
y_values.append([v_to_db(data[f]) for f in data])
elif mesh_node.vibration_level_unit == 'decibel' and amplitude_type == 'velocity':
y_values.append([db_to_v(data[f]) for f in data])
if mep_limits:
labels.append(f'SA base vibration limit {amplitude_type}')
x_values.append(list(mep_limits[list(mep_limits.keys())[1]]))
y_values.append(list(mep_limits[list(mep_limits.keys())[2]]))
if amplitude_type == 'velocity':
y_label = 'Velocity amplitude [m/s]'
elif amplitude_type == 'decibel':
y_label = 'Velocity level amplitude [dB]'
else:
raise NotImplementedError(
f"ERROR: Amplitude_type {amplitude_type} is not available, please select from 'velocity' or 'decibel'.")
if target_name:
plot_title = f'{amplitude_type.capitalize()} Spectrum of Target {target_name}'
else:
plot_title = f'{amplitude_type.capitalize()} Spectrum of Mesh Node {node}'
if plot_components:
plot_title += ' and Components'
if mep_limits:
plot_title += f' with limit {amplitude_type}'
return plot_function(
x_values=x_values, y_values=y_values, labels=labels, title=plot_title, x_label=f'Frequency [Hz]',
y_label=y_label, log_scale_x=log_scale_x, log_scale_y=log_scale_y, x_lim=None, y_lim=None,
file=project.results_folder, show=show, line_width=line_width, plot_max_values=False, horizontal_line=None)
[docs]
def plot_f_spectrum_at_multiple_nodes(
project, nodes: list[Union[MeshNode, list[float]]], target_names: list, target_type: Optional[str] = None,
mep_limits: dict = None, amplitude_type: Literal['decibel', 'velocity'] = 'velocity', log_scale_x: bool = False,
log_scale_y: bool = False, show: bool = False, line_width: float = 1) -> Path:
"""
Plot frequency spectrum at multiple nodes in one graph.
Input:
- project (obj): Project object containing collections of objects and project variables.
- nodes (list of MeshNode or list of float): Either a list of `MeshNode` or a list of coordinates x, y, (z)
used to look up the node on the mesh.
- target_names (list of str): Input for the name of the targets for labels.
- target_type (str): Input for the type of the targets. Default value is None.
- mep_limits (dict): Specify the compliance limits used in the plot. Default value is None.
- amplitude_type (str): Amplitude domain, select from 'velocity' or 'decibel'. Used for total level per
frequency. Default value is 'velocity'.
- log_scale_x, log_scale_y (bool): Select to apply logarithmic scaling for the x and y-axis. Default value is
False, no logarithmic scaling.
- show (bool): Select to display the figure window in GUI. Default value is False.
- line_width (float): Specify the line width used in the plot. Default value is 1.
Output:
- Creates the spectra for multiple nodes in one graph at the requested location and returns the file path of the
created png-file. File is saved in the results folder specified for the project.
"""
if not isinstance(nodes, list) or not nodes:
raise ValueError("ERROR: The 'nodes' were not provided.")
if not (all(isinstance(node, list) for node in nodes) or all(isinstance(node, MeshNode) for node in nodes)):
raise ValueError(
"ERROR: The 'nodes' list must be homogeneous, containing ONLY lists of coordinates ([x, y]) OR ONLY "
"MeshNode objects.")
x_values = []
y_values = []
labels = []
for node in nodes:
if isinstance(node, list) and all(isinstance(coord, (float, int)) for coord in node):
mesh_node = project.mesh.get_mesh_node_by_coordinates_2d(node)
elif isinstance(node, MeshNode):
mesh_node = node
else:
continue
# Total level per frequency
total_level_per_frequency = mesh_node.calculate_total_level_per_frequency(amplitude_type=amplitude_type)
x_values.append(list(total_level_per_frequency.keys()))
y_values.append([total_level_per_frequency[f] for f in total_level_per_frequency])
labels.append(f'Target {target_names[nodes.index(node)]}')
if mep_limits:
labels.append(f'SA base vibration limit {target_type}')
x_values.append(list(mep_limits[list(mep_limits.keys())[1]]))
y_values.append(list(mep_limits[list(mep_limits.keys())[2]]))
if amplitude_type == 'velocity':
y_label = 'Velocity amplitude [m/s]'
elif amplitude_type == 'decibel':
y_label = 'Velocity level amplitude [dB]'
else:
raise NotImplementedError(
f"ERROR: Amplitude_type {amplitude_type} is not available, please select from 'velocity' or 'decibel'.")
if target_type is not None:
plot_title = f'{amplitude_type[0].upper() + amplitude_type[1:]} Spectrum at all targets of {target_type}'
else:
plot_title = f'{amplitude_type[0].upper() + amplitude_type[1:]} Spectrum at all targets'
x_lim = None
y_lim = None
if mep_limits:
if 'LF-' in target_type:
x_lim = [3, 40]
y_lim = [0, 140]
elif 'HF-' in target_type:
x_lim = [30, 100]
y_lim = [0, 140]
return plot_function(
x_values=x_values, y_values=y_values, labels=labels, title=plot_title, x_label=f'Frequency [Hz]',
y_label=y_label, log_scale_x=log_scale_x, log_scale_y=log_scale_y, x_lim=x_lim, y_lim=y_lim,
file=project.results_folder, show=show, line_width=line_width, plot_max_values=False, horizontal_line=None)
### ===================================================================================================================
### 4. End of script
### ===================================================================================================================