Source code for haskoning_atr_tools.vibration_contour_plot.plot.plot_functions

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