Source code for haskoning_atr_tools.human_induced_vibrations.reporting

### ===================================================================================================================
###   ATR TOOLS
###   Reporting HUMAN INDUCED VIBRATIONS MODULE
### ===================================================================================================================
# Copyright ©2026 Haskoning Nederland B.V.

### ===================================================================================================================
###   1. Import modules
### ===================================================================================================================

# Import general functions
from typing import List, Optional, Dict, Union
from pathlib import Path
from warnings import warn
from json import dump
from math import isclose
from datetime import datetime
from getpass import getuser

# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.human_induced_vibrations.step_walking_load import StepWalkingLoad
from haskoning_atr_tools.config import Config
from haskoning_atr_tools.enums import SecurityClassificationSpecification

# References for functions and classes in the haskoning_structural package
from haskoning_atr_tools import installed_modules
if 'haskoning_structural' in installed_modules:
    from haskoning_structural.fem_status import Project as FemProject
    from haskoning_structural.analyses import Analysis
    from haskoning_structural.mesh import MeshNode
    from haskoning_structural.output_items import OutputItem
    from haskoning_structural.fem_tools import fem_create_folder
    haskoning_structural_use = True
else:
    FemProject = None
    Analysis = None
    MeshNode = None
    OutputItem = None
    haskoning_structural_use = False
if 'haskoning_datafusr_py_base' in installed_modules:
    from haskoning_datafusr_py_base.reporting import datafusr_create_report
    from haskoning_datafusr_py_base.reporting.template_processor import TemplateProcessor
    haskoning_datafusr_py_base_use = True
else:
    datafusr_create_report = None
    TemplateProcessor = None
    haskoning_datafusr_py_base_use = False

# Import third-party libraries
import matplotlib
matplotlib.use(Config.MPL_NONINTERACTIVE(notify=False))
import matplotlib.pyplot as plt


### ===================================================================================================================
###   2. Create reporting functions
### ===================================================================================================================

[docs] def create_walking_load_reporting_data( project: FemProject, analysis: Analysis, step_walking_load: StepWalkingLoad, mesh_nodes: List[MeshNode], directions: List[str], loaded_node: MeshNode, add_zeros: bool = False, reporting_folder: Optional[Path] = None, author: Optional[str] = None, reference_code: Optional[str] = None, security_classification: str = 'project related', project_number: Optional[str] = None, report_name: str = 'Step walking load') -> Dict: """ Function to create reporting data of the response of the provided mesh nodes in the provided directions for the provided analysis and step walking load. Input: - project (obj): The fem project in which the analysis and step walking load are defined. - analysis (obj): The analysis for which the report should be created. - step_walking_load (boj): The step walking load for which the report should be created. - mesh_nodes (list of obj): The mesh nodes for which the report should be created. - directions (list of str): The directions for which the report should be created. Provide as a list of one or more from ['x', 'y', 'z']. - loaded_node (obj): The node that is loaded with the step walking load. - add_zeros (bool): Whether to add a zero value at the beginning of the response curve if not already present. This can be useful to visualize the response starting from zero before the load is applied. Defaults to False. - reporting_folder (Path, optional): The folder where the report files will be saved. If not provided, the current working directory will be used. - author (str): Name of the author of the report. Optional argument. Default value is None, in which case the name is retrieved from the system. If the user does not want to add to report, provide an empty string. - reference_code (str): Reference code for the report. Optional argument. Default value is None, in which case the project number is used if provided. If both are provided, the reference code is used. The reference code is the full code used in the company for reporting. - security_classification (str): Security classification for the report. Default value is 'project related'. User can select any of the Haskoning security classification specifications. - project_number (str): Project number for the report. Optional argument. - report_name (str): Name of the report that will be created. This name will be used for the file and extended with the current date and time. Default is Step walking load. Output: - A dict with the reporting data """ def single_plot(x_y_data: List[List[float]], mesh_node: MeshNode, output_item: OutputItem, save_file: Path): # Create the plot plt.close() plt.style.use(Config.PLOT_STYLE_FILE.as_posix()) plt.plot(x_y_data[0], x_y_data[1]) plt.xlabel('Time [s]') plt.ylabel('Velocity [m/s]') plt.ticklabel_format(style='sci', axis='y') # plt.title( # f'Response in {output_item.component}-direction for mesh node {mesh_node.id} ' # f'{[round(_, 5) for _ in mesh_node.coordinates]}') plt.grid(True) plt.savefig(fname=save_file.as_posix(), bbox_inches='tight', dpi=150) plt.close() if not haskoning_structural_use: raise ImportError( f"ERROR: The haskoning_structural package is not installed. Please install it to use the reporting " f"functions.") if not isinstance(analysis, Analysis): raise TypeError( f"ERROR: The provided analysis is not an instance of the {Analysis.__name__} class. Please provide a valid " f"{Analysis.__name__} instance. Provided was {analysis} of type {type(analysis)}.") if not isinstance(step_walking_load, StepWalkingLoad): raise TypeError( f"ERROR: The provided step walking load is not an instance of the {StepWalkingLoad.__name__} class. Please " f"provide a valid {StepWalkingLoad.__name__} instance. Provided was {step_walking_load} of type " f"{type(step_walking_load)}.") if not isinstance(mesh_nodes, list): raise TypeError( f"ERROR: The provided mesh nodes are not provided as a list. Please provide the mesh nodes as a list of " f"{MeshNode.__name__} instances. Provided was {mesh_nodes} of type {type(mesh_nodes)}.") if not all(isinstance(node, MeshNode) for node in mesh_nodes): raise TypeError( f"ERROR: Not all provided mesh nodes are instances of the {MeshNode.__name__} class. Please provide the " f"mesh nodes as a list of {MeshNode.__name__} instances. Provided was {mesh_nodes} with types " f"{[type(node) for node in mesh_nodes]}.") if not isinstance(directions, list): raise TypeError( f"ERROR: The provided directions are not provided as a list. Please provide the directions as a list of " f"strings. Provided was {directions} of type {type(directions)}.") valid_direction = ['x', 'y', 'z', '-x', '-y', '-z'] if not all(direction.lower() in valid_direction for direction in directions): raise TypeError( f"ERROR: Not all provided directions valid. Please provide the directions as a list of one or more from " f"{valid_direction}. Provided was {directions}.") if any('x' in direction.lower() for direction in directions) or \ any('y' in direction.lower() for direction in directions): warn(f"WARNING: The requested directions contain at least one horizontal direction. According to the SBR the " f"structure is loaded with only a vertical load. Horizontal output may give unrealistic low values.") # Security classification is to be selected from available options if security_classification not in [cs.value for cs in SecurityClassificationSpecification]: raise ValueError( f"ERROR: An invalid security classification has been provided ({security_classification}). " f"Please select from {[cs.value for cs in SecurityClassificationSpecification]}.") output_items = [ oi for oi in project.collections.velocity_models if oi.component in directions] analysis_references = [ ar for ar in project.collections.stepped_analysis_reference if ar.analysis == analysis and ar.step_nr] analysis_references.sort(key=lambda x: x.step_nr) analysis_references_time = [ar for ar in analysis_references if ar.meta_data and 'time' in ar.meta_data] if analysis_references_time: analysis_references = analysis_references_time time_steps = {ar.step_nr: ar.meta_data['time'] for ar in analysis_references} else: # todo add procedure to extract time steps from the analysis settings if not provided in the meta data of the analysis references raise ValueError( f"ERROR: No time info found in the meta data of the analysis reference.") if reporting_folder is None: reporting_folder = Path().cwd() elif isinstance(reporting_folder, str): reporting_folder = Path(reporting_folder) if not reporting_folder.exists(): fem_create_folder(reporting_folder) plot_path = reporting_folder / f"input_load_plot.png" step_walking_load_plot = step_walking_load.plot(show=False, normalised=False, save_file=plot_path, add_title=False) mesh_node_coordinates = {mesh_node.id: [round(_, 5) for _ in mesh_node.coordinates] for mesh_node in mesh_nodes} data = { 'name': step_walking_load.name, 'body_mass': step_walking_load.step_load.body_mass, 'step_frequency': step_walking_load.step_load.step_frequency, 'nr_steps': step_walking_load.nr_steps, 'load_reduction': step_walking_load.load_reduction, 'step_size': step_walking_load.step_size, 'method': step_walking_load.method, 'total_walking_time': round(step_walking_load.total_walking_time, 2), 'interval': step_walking_load.interval, 'step_walking_load_plot': step_walking_load_plot.as_posix(), 'mesh_nodes': ", ".join([f"{mesh_node.id} {mesh_node_coordinates[mesh_node.id]}" for mesh_node in mesh_nodes]), 'mesh_node_coordinates': mesh_node_coordinates, 'directions': ', '.join(directions), 'loaded_node': f"{loaded_node.id} {[round(_, 5) for _ in loaded_node.coordinates]}", 'report_date': datetime.now().strftime('%d %B %Y'), 'report_name': report_name, 'author': author if author else getuser(), 'security_classification': security_classification.capitalize(), } # Set the reference code if reference_code: data['reference_code'] = reference_code elif project_number: data['reference_code'] = project_number else: data['reference_code'] = '' results = {} for mesh_node in mesh_nodes: results[mesh_node] = {} shapes = mesh_node.shapes for shape in shapes: for output_item in output_items: if output_item in results[mesh_node]: continue output_item_results = {} for analysis_reference in analysis_references: result_dictionary = shape.results.get_result_dict( output_item=output_item, analysis_reference=analysis_reference) if result_dictionary is None: break value = result_dictionary.results.get(mesh_node, None) if value is not None: output_item_results[analysis_reference.step_nr] = value else: # not complete results for this node in these shape results for this output item break else: results[mesh_node][output_item] = output_item_results if not results[mesh_node]: warn( f"WARNING: No results found for mesh node {mesh_node}. Please check if the provided mesh node is " f"correct and if the analysis has been run successfully.") del results[mesh_node] elif not all(output_item in results[mesh_node] for output_item in output_items): warn( f"WARNING: Not all output items have results for mesh node {mesh_node}. Only found results for " f"{list(results[mesh_node].keys())} while expected results for {output_items}.") x_y_data = {} x_y_plots = {} x_y_json = {} for mesh_node, output_items_results in results.items(): x_y_data[mesh_node] = {} x_y_plots[mesh_node.id] = {} x_y_json[str(mesh_node)] = {} for output_item, output_item_results in output_items_results.items(): x, y = [], [] for step_nr, value in output_item_results.items(): x.append(time_steps[step_nr]) y.append(value) if add_zeros and not isclose(x[0], 0.0): x.insert(0, 0.0) y.insert(0, 0.0) x_y_data[mesh_node][output_item] = [x, y] x_y_json[str(mesh_node)][str(output_item)] = [x, y] save_file = reporting_folder / f"response_{output_item.component}_{mesh_node.id}.png" x_y_plots[mesh_node.id][output_item.component] = save_file.as_posix() single_plot(x_y_data=[x, y], mesh_node=mesh_node, output_item=output_item, save_file=save_file) data['plots_per_node_per_direction'] = x_y_plots json_dump = reporting_folder / f"response_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json" with open(json_dump.as_posix(), 'w') as f: dump(x_y_json, f, indent=2, sort_keys=True) # Combined plot of all output items per mesh node plots_per_node_all_directions = {} for mesh_node, output_items_results in results.items(): if len(output_items_results) > 1: output_item_directions = [output_item.component for output_item in output_items_results] string_directions = '-'.join(output_item_directions) plt.close() plt.style.use(Config.PLOT_STYLE_FILE.as_posix()) fig, ax = plt.subplots() save_file = reporting_folder / f"response_{mesh_node.id}_{string_directions}_dir.png" ax.set_xlabel(f'Time in [s]') ax.set_ylabel(f'Velocity in [m/s]') for i, output_item in enumerate(output_items_results.keys()): ax.plot( x_y_data[mesh_node][output_item][0], x_y_data[mesh_node][output_item][1], label=f'Direction {output_item.component}') ax.legend(loc='center left', bbox_to_anchor=(1, 0.5)) ax.ticklabel_format(style='sci', axis='y') plt.savefig(fname=save_file.as_posix(), bbox_inches='tight', dpi=150) plt.close() plots_per_node_all_directions[mesh_node.id] = { 'plot': save_file.as_posix(), 'directions': string_directions, 'coordinates': mesh_node_coordinates[mesh_node.id]} if plots_per_node_all_directions: data['plots_per_node_all_directions'] = plots_per_node_all_directions # Combined plot of all mesh nodes per output item plots_all_nodes_per_direction = {} if not len(results) == 1: _mesh_nodes = [] for output_item in output_items: _mesh_nodes = [mesh_node for mesh_node in results if output_item in results[mesh_node]] if not _mesh_nodes: continue plt.close() plt.style.use(Config.PLOT_STYLE_FILE.as_posix()) fig, ax = plt.subplots() save_file = reporting_folder / f"response_{output_item.component}_all_nodes.png" string_node = ', '.join([f'{mesh_node.id}' for mesh_node in _mesh_nodes]) ax.set_xlabel(f'Time in [s]') ax.set_ylabel(f'Velocity in [m/s]') for i, _mesh_node in enumerate(_mesh_nodes): ax.plot( x_y_data[_mesh_node][output_item][0], x_y_data[_mesh_node][output_item][1], label=f'mesh node {_mesh_node.id} {mesh_node_coordinates[_mesh_node.id]}') ax.legend(loc='center left', bbox_to_anchor=(1, 0.5)) ax.ticklabel_format(style='sci', axis='y') plt.savefig(fname=save_file.as_posix(), bbox_inches='tight', dpi=150) plt.close() plots_all_nodes_per_direction[output_item.component] = { 'plot': save_file.as_posix(), 'mesh_nodes': string_node} if plots_all_nodes_per_direction: data['plots_all_nodes_per_direction'] = plots_all_nodes_per_direction # Create the json dump of the data that is used for the report. This can be used for traceability and to easily # create a new report with the same data in the future if needed. json_dump = reporting_folder / f"report_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json" with open(json_dump.as_posix(), 'w') as f: dump(data, f, indent=2, sort_keys=True) return data
[docs] def create_walking_load_report(data: Dict, reporting_folder: Optional[Path] = None) -> Optional[TemplateProcessor]: """ Function to create reporting data of the response of the provided mesh nodes in the provided directions for the provided analysis and step walking load. Input: - data (dict): The data that is used for the report. This data can be created with the create_walking_load_reporting_data function. - reporting_folder (Path, optional): The folder where the report files will be saved. If not provided, the current working directory will be used. Output: - Creates and returns the py-object containing the report information. """ if not haskoning_datafusr_py_base_use: raise ImportError( f"ERROR: The haskoning_datafusr_py_base package is not installed. Please install it to use the reporting " f"functions.") if reporting_folder is None: reporting_folder = Path().cwd() output_file = reporting_folder / f"response_report_{datetime.now().strftime('%Y%m%d%H%M%S')}.docx" template_file = Path(__file__).parent / 'templates' / 'human_induced_vibrations.docx' return datafusr_create_report(template_file=template_file, output_file=output_file, data=data)
### =================================================================================================================== ### 3. End of script ### ===================================================================================================================