Source code for viiapackage.results.result_functions.viia_acceleration_graphs

### ===================================================================================================================
###   Function to create acceleration graphs
### ===================================================================================================================
# Copyright ©VIIA 2024

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

# General imports
from __future__ import annotations
import re
import json
from collections import namedtuple
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Dict, Tuple

# References for functions and classes in the DataFusr py-base package
from datafusr_py_base.deprecation import deprecation_input_error

# References for functions and classes in the rhdhv_fem package
from rhdhv_fem.analyses import AnalysisReference, Analysis
from rhdhv_fem.fem_math import fem_numerical_differentiation
from rhdhv_fem.fem_config import Config
from rhdhv_fem.mesh import MeshNode
from rhdhv_fem.output_items import DisplacementOutputItem

# References for functions and classes in the viiaPackage
if TYPE_CHECKING:
    from viiapackage.viiaStatus import ViiaProject
from viiapackage.analyses.helper_functions import viia_find_3_mesh_nodes_on_wall
from viiapackage.viiaGeneral import viia_find_closest_mesh_node

# Import module matplotlib, check if module is installed
import matplotlib
# Switch to a non-interactive backend to properly import matplotlib.pyplot
matplotlib.use(Config.MPL_NONINTERACTIVE(notify=False))
import matplotlib.pyplot as plt


### ===================================================================================================================
###   2. Function viia_acceleration_graphs
### ===================================================================================================================

[docs]@deprecation_input_error( package='viiapackage', since='70.5', old_arg_types=[bool], old_arg_names=['numerical_differentiation']) def viia_acceleration_graphs( project: ViiaProject, tb_file: Path, signal: str, analysis: Analysis, acceleration_nodes: Optional[List[int]] = None, final_timestep: Optional[int] = None, diff_gap: int = 1) -> Optional[List[Path]]: """ For a given signal, this function generates three graphs that present the acceleration of the building in x-, y- and z-directions. The acceleration is plotted for three locations; Base acceleration, foundation acceleration and far field acceleration. For foundation and top of building, a node must be provided from which to extract the acceleration data. .. note:: The accelerations are calculated based on numerical differentiation of the displacements of the mesh-nodes Be aware that the tb-file values may contain unexpected noise. Input: - project (obj): Project object containing collections and of fem objects and project variables. - tb_file_loc (Path): Location of the tb-file. - signal (str): String representing the signal, can be S1 to S11. - analysis (obj): Object reference of analysis of the results in the tb-file. - acceleration_nodes (list of int): A list of nodes that user specified to collect acceleration, these nodes are specified by the ID of the mesh-node. - final_timestep (int): Optional timestep up to which to plot the signal and the accelerations. - diff_gap (int): Gap between the two consecutive time steps considered for numerical differentiation. Default value 1. Only required if numerical differentiation is applied. Output: - This function generates three plots in a new folder 'A12 Acceleration Graphs', describing the acceleration of the building in x-, y- and z-direction in three different locations. The folder and the three files are return in a tuple (order: folder, x, y- and z-graph). """ # Check if tb-file is present if '_OUTPUT_3.tb' not in tb_file.as_posix() or not tb_file.exists(): project.write_log( "WARNING: Input file not recognised for acceleration function, check your input. No graphs are generated " "for the accelerations.") return None # Check the signal number if signal not in [f"S{i}" for i in range(1, 12)]: project.write_log( f"WARNING: Signal needs to be between S1-S11, current value {signal} is not allowed. " f"No acceleration graphs generated.") return None # Create container to store node displacement/acceleration/name all_data = [] time_steps = None acc_data = namedtuple('Acc_Data', ['name', 'node', 'time_steps', 'disp', 'accel']) # Collect the results from the DIANA tb-file project.read_diana_tbfile(file=tb_file, analysis=analysis) # Get results mesh-nodes and output-items for output_block in analysis.get_all_output_blocks(): if output_block.name == 'OUTPUT_3': output_items = output_block.output_items result_mesh_nodes = output_block.manual_nodes break else: raise LookupError(f"ERROR: No OUTPUT_3 output block found in {analysis.name}.") # Filter and sort output items output_items = [ output_item for output_item in output_items if isinstance(output_item, DisplacementOutputItem) and output_item.component != 'resultant'] output_items = sorted(output_items, key=lambda output_item: output_item.component) # Get, filter and sort analysis references calculation_block = [ calculation_block for calculation_block in analysis.calculation_blocks if getattr(calculation_block, 'output_blocks', []) and output_block in getattr(calculation_block, 'output_blocks', [])] if len(calculation_block) != 1: raise LookupError( f"ERROR: Multiple possible calculation_blocks found ({len(calculation_block)}). OUTPUT_3 output is " f"specified in multiple calculation blocks this is not allowed.") calculation_block = calculation_block[0] analysis_references = [ analysis_reference for analysis_reference in project.collections.stepped_analysis_reference if analysis_reference.analysis == analysis and analysis_reference.calculation_block == calculation_block and analysis_reference.historic_envelope is None and analysis_reference.meta_data is not None] analysis_references = sorted(analysis_references, key=lambda analysis_ref: analysis_ref.step_nr) default_nodes = [] name_mapping = {} if not acceleration_nodes: # No user defined nodes, default will be selected acceleration_nodes_dict = viia_get_accel_graph_mesh_nodes(project=project) default_nodes = [i for v in acceleration_nodes_dict.values() for i in v] name_mapping = {i: k for k, v in acceleration_nodes_dict.items() for i in v} # Collect the time steps _time_steps = [analysis_reference.meta_data['time'] for analysis_reference in analysis_references] # Get all nodes in tb-file for result_mesh_node in result_mesh_nodes: disp = _viia_get_disp_result_mesh_node(result_mesh_node, output_items, analysis_references) time_steps, accel = _viia_differentiate_acceleration_numerically(_time_steps, *disp, diff_gap=diff_gap) if result_mesh_node in default_nodes and result_mesh_node in name_mapping: name = name_mapping[result_mesh_node] else: name = 'node' all_data.append(acc_data(name=name, node=result_mesh_node, time_steps=time_steps, disp=disp, accel=accel)) # Retrieve the base accelerations time_steps_x, base_acceleration_x = _get_base_acceleration(project, signal=signal, direction='X', unit='m/s2') time_steps_y, base_acceleration_y = _get_base_acceleration(project, signal=signal, direction='Y', unit='m/s2') time_steps_z, base_acceleration_z = _get_base_acceleration(project, signal=signal, direction='Z', unit='m/s2') # Check the length of the time-steps list if not all([len(time_steps_x) == len(lst) for lst in [time_steps_y, time_steps_z]]): raise ValueError(f"ERROR: Length of the three base acceleration lists should match.") # Collect the index of the final timestep index = None if time_steps and final_timestep: index = time_steps_x.index(time_steps[final_timestep]) # Create the folder to store the acceleration graphs output_folder = project.current_analysis_folder / project.viia_settings.DEFAULT_ACCELERATION_GRAPHS output_folder.mkdir(parents=True, exist_ok=True) # Create the graphs collected_files = [ # Create the graph for accelerations in the x-direction _create_acceleration_plot( project=project, x_values=time_steps_x[:index], y_values=base_acceleration_x[:index], data=all_data, direction='x', final_timestep=final_timestep, signal=signal, output_folder=output_folder), # Create the graph for accelerations in the y-direction _create_acceleration_plot( project=project, x_values=time_steps_y[:index], y_values=base_acceleration_y[:index], data=all_data, direction='y', final_timestep=final_timestep, signal=signal, output_folder=output_folder), # Create the graph for accelerations in the z-direction _create_acceleration_plot( project=project, x_values=time_steps_z[:index], y_values=base_acceleration_z[:index], data=all_data, direction='z', final_timestep=final_timestep, signal=signal, output_folder=output_folder)] # Store the data in separate json-file json_file = _viia_create_acceleration_json(project=project, data={ 'x': {'time': time_steps_x, 'accelerations': base_acceleration_x}, 'y': {'time': time_steps_y, 'accelerations': base_acceleration_y}, 'z': {'time': time_steps_z, 'accelerations': base_acceleration_z}}) collected_files.append(json_file) # Notify user of finishing the acceleration graphs project.write_log(f"Acceleration graphs for signal {signal} created in {output_folder.as_posix()}.") # Return list of created graphs return collected_files
### =================================================================================================================== ### 3. Function to create the accelerations json-file for VIIA ### ===================================================================================================================
[docs]def _viia_create_acceleration_json(project: ViiaProject, data: Dict[str, Dict[str, List[float]]]) -> Path: """ Function to store the data of the accelerations (in VIIA format) in a json-file in the current analysis folder. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - data (dict): Dictionary with the data of the graphs per direction. Output: - Generates a json-file in the current analysis-folder with the data of the accelerations. - Returns the path of the file created. """ # Create json-file dump in the current analysis folder if project.current_analysis_folder is None: raise ValueError("ERROR: The analysis folder was not set correctly, please provide correct folder.") dumpfile = project.current_analysis_folder / 'accelerations.json' with open(dumpfile, 'w') as fd: json.dump(data, fd, indent=2, sort_keys=True) return dumpfile
### =================================================================================================================== ### 4. Helper Functions ### ===================================================================================================================
[docs]def viia_get_accel_graph_mesh_nodes( project: ViiaProject, has_foundation_nodes: Optional[bool] = False) -> Dict[str, List[MeshNode]]: """ This function returns a set of default mesh-nodes by selecting one node from each floor, one node from pile or shallow foundation strip and optional foundation wall nodes. Input: - project (obj): Project object containing collections and of fem objects and project variables. - has_foundation_nodes (bool): If true, the foundation wall middle nodes will be selected, else not. Output: - Returns a dictionary containing mesh-node objects for 'foundation_nodes', 'floor_nodes', 'pile_strip_nodes' and 'fstrip_nodes' (as keys). The values are a list of MeshNode objects. """ # Dictionary to store the relation nodes_name = { 'foundation_nodes': [], 'floor_nodes': [], 'pile_strip_nodes': [], 'fstrip_nodes': []} # Pick one node per floor all_floor_nodes = project.viia_find_floor_mesh_nodes() flr_nodes = {k: v[0] for k, v in all_floor_nodes.items()} nodes_name['floor_nodes'].extend(list(flr_nodes.values())) project.write_log( f"The floor nodes {flr_nodes} have been selected for output 3, please make sure that those nodes are " f"not connected with nonlinear interfaces.") # If pile exist for spring in project.collections.springs: # The first Huan beam if 'PAAL' in spring.name: # Take the top point of a pile spring as it is on the foundation strip node_coord = sorted(spring.get_points(), key=lambda x: x[-1], reverse=True)[0] pile_strip_node = viia_find_closest_mesh_node(project=project, target_point=node_coord) nodes_name['pile_strip_nodes'].append(pile_strip_node) project.write_log(f'The pile strip node {pile_strip_node} from {spring} have been selected for output 3.') break # Take a node from fstrip from viiapackage.analyses.helper_functions import viia_find_shallow_foundation_elements # Take the second item in return tuple shallow_foundation_nodes = viia_find_shallow_foundation_elements(project=project)[1] if shallow_foundation_nodes['support_obj']: fstrip_node = shallow_foundation_nodes['support_obj'][0] nodes_name['fstrip_nodes'].append(fstrip_node) project.write_log(f"The fstrip node {fstrip_node} with id {fstrip_node.id} have been selected for output 3.") if has_foundation_nodes: for wall in project.collections.walls: if 'FUND' in wall.name: # First item in return, second item for mid-node has_foundation_nodes = viia_find_3_mesh_nodes_on_wall(project=project, wall=wall)[0][1] nodes_name['foundation_nodes'].append(has_foundation_nodes) project.write_log( f"The foundation middle nodes have been selected for output 3, please double check if those " f"nodes are selected correctly in DIANA.") return nodes_name
[docs]def _get_base_acceleration(project: ViiaProject, signal: str, direction: str, unit: str = 'm/s2') \ -> Tuple[List[float], List[float]]: """ Function that retrieves the base acceleration for a given signal and direction from the project at all time-steps. Input: - signal (str): Name of the signal, can be S1-S11. For example 'S4'. - direction (str): Either x, y or z. - unit (str): Can be either in 'G' or in 'm/s2'. Default value is 'm/s2'. Output: - Returns tuple with list of time-steps (in seconds) for all the available data points and list of base acceleration values for the given signal and direction corresponding to these time-steps. """ # Check the signal number, should be in range S1-S11 signal_number = int(re.search(r'\d+', signal).group()) if signal_number not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]: raise ValueError(f"Signal should be in range S1-S11, not S{signal_number}.") # Collect the base accelerations and time-steps from the signal current_cluster = project.project_information['cluster_nen'] base_acceleration_factors = [] base_acceleration_time_steps = [] for timeseries_combination_set in project.collections.timeseries_combination_sets: if timeseries_combination_set.name.split('_')[-1] == 'cluster' + current_cluster: base_acceleration_factors = getattr( timeseries_combination_set.get_timeseries_combination(f'base_signal_{signal}'), f'{direction.lower()}_direction').value_series base_acceleration_time_steps = getattr( timeseries_combination_set.get_timeseries_combination(f'base_signal_{signal}'), f'{direction.lower()}_direction').time_series if len(base_acceleration_factors) == 0: raise ValueError( f"ERROR: No base acceleration in {direction.lower()}-direction found in the project. Did you run " f"viia_add_basemotion_signals?") if len(base_acceleration_time_steps) == 0: raise ValueError(f"ERROR: Time steps for signal {signal_number} not found in project, check base signals.") # Multiply the acceleration factors with the peak ground acceleration to obtain accelerations peak_ground_acceleration = project.project_information['pga'] base_acceleration = [ base_acceleration_factor * peak_ground_acceleration * project.importance_factor for base_acceleration_factor in base_acceleration_factors] # By default, the values are stored in [g], convert them to [m/s2] when required if unit.lower() not in ['g', 'm/s2']: raise ValueError(f"ERROR: Unit {unit} not supported, choose either 'g' or 'm/s2'.") if unit.lower() == 'm/s2': base_acceleration = [value * project.gravitational_acceleration for value in base_acceleration] return base_acceleration_time_steps, base_acceleration
[docs]def _create_acceleration_plot( project: ViiaProject, x_values: List[float], y_values: List[float], data: list, direction: str, final_timestep: int, signal: str, output_folder: Path) -> Path: """ This function creates the acceleration plot.""" # Initiate plot plt.close() # Apply VIIA graph style plt.style.use(project.viia_settings.graph_style_sheet.as_posix()) # Plot the values fig, ax = plt.subplots(figsize=(project.viia_settings.GRAPH_WIDTH, project.viia_settings.GRAPH_HEIGHT)) ax.plot(x_values, y_values, alpha=0.7, label='Base') # Set the axes labels ax.set_xlabel('Time Step [s]') ax.set_ylabel('Acceleration [m/s\u00B2]') fig.suptitle(f'{signal}: Acceleration in {direction}-direction') # Plot all additional nodes index = ['x', 'y', 'z'].index(direction) for plot_data in data: ax.plot(plot_data.time_steps[:final_timestep], plot_data.accel[index][:final_timestep], label=f'{plot_data.name} (node {plot_data.node.id})') # Add a legend ax.legend(markerscale=4) # Create the graph file = output_folder / f'{signal}_{direction}.png' fig.savefig(file, format='png') plt.close() # Return the file with the image of the graph that was created return file
[docs]def _create_displacement_plot( project: ViiaProject, x_values: List[float], y_values: List[float], data: list, direction: str, final_timestep: int, signal: str, output_folder: Path) -> Path: """ This function creates the displacement plot.""" # Initiate plot plt.close() # Apply VIIA graph style plt.style.use(project.viia_settings.graph_style_sheet.as_posix()) # Plot the values fig, ax = plt.subplots(figsize=(project.viia_settings.GRAPH_WIDTH, project.viia_settings.GRAPH_HEIGHT)) ax.scatter(x_values, y_values, s=3, c='green', label='Base') ax.plot(x_values, y_values, c='green', alpha=0.7) # Set the axes labels ax.set_xlabel('Time Step [s]') ax.set_ylabel('Displacements [m]') fig.suptitle(f'{signal}: Displacements in {direction}-direction') # Add a legend ax.legend(markerscale=4) # Plot all additional nodes index = ['x', 'y', 'z'].index(direction) for plot_data in data: ax.plot(plot_data.timesteps[:final_timestep], plot_data.disp[index][:final_timestep], label=f'{plot_data.name} (node {plot_data.node.id})') # Create the graph file = output_folder / f'{signal}_{direction}_displacements.png' fig.savefig(file, format='png') plt.close() # Return the file with the image of the graph that was created return file
[docs]def _viia_differentiate_acceleration_numerically( time_steps: List[float], displacements_x: List[float], displacements_y: List[float], displacements_z: List[float], differentiation_scheme: str = 'forward-difference', diff_gap: int = 1): """ This function uses the numerical differentiation technique from fem-math module to determine the accelerations, using the datapoints for displacement from the tb-file. The user can select differentiation schema and select the differentiation gap. A check is performed if the Nyquist frequency is not below 25Hz. """ # Check the Nyquist frequency dt = (time_steps[1] - time_steps[0]) * diff_gap nyq_freq = 1 / (2 * dt) if nyq_freq < 25: raise ValueError( f"ERROR: The Nyquist frequency is too low {round(nyq_freq, 1)} < 25 Hz. To increase the Nyquist frequency " f"you should use a lower diff_gap input.") # Execute the differentiation time_steps_x, accelerations_x = fem_numerical_differentiation( time_steps=time_steps, data_points=displacements_x, differentiation_scheme=differentiation_scheme, diff_gap=diff_gap, order=2) time_steps_y, accelerations_y = fem_numerical_differentiation( time_steps=time_steps, data_points=displacements_y, differentiation_scheme=differentiation_scheme, diff_gap=diff_gap, order=2) time_steps_z, accelerations_z = fem_numerical_differentiation( time_steps=time_steps, data_points=displacements_z, differentiation_scheme=differentiation_scheme, diff_gap=diff_gap, order=2) # Check if the time-steps are and remained the same length assert time_steps_x == time_steps_y == time_steps_z # Return the accelerations, calculated based on the input displacements return time_steps_x, (accelerations_x, accelerations_y, accelerations_z)
[docs]def _viia_get_disp_result_mesh_node( result_mesh_node: MeshNode, output_items: List[DisplacementOutputItem], analysis_references: List[AnalysisReference]): """ This function retrieves the displacement results for a result_mesh_node. The function searches through the shapes connected to the result_mesh_node and selects the shape with the results. After selection, displacement results are retrieved for all output_items and all analysis_references.""" # Find the shape with the results for every output_item software = 'diana' disp = [] result_check = None result_shape = None for shape in result_mesh_node.shapes + result_mesh_node.connections: # Check for the first output item and analysis reference if the shape contains the results # Assumption: Displacement results per result mesh node are stored in one shape, # for all output items and for all analysis references # Skip check if no results dictionary is present at all if not shape.results: continue result_check = shape.results.get_result_value( output_item=output_items[0], analysis_reference=analysis_references[0], software=software, mesh_element=None, mesh_node=result_mesh_node) # Break loop if results are found if result_check is not None: result_shape = shape break # Raise error if no results are found if result_check is None: raise LookupError(f"No results found for {result_mesh_node} in any of the connecting shapes.") for output_item in output_items: # Add displacements disp.append([result_shape.results.get_result_value( output_item=output_item, analysis_reference=analysis_reference, software=software, mesh_element=None, mesh_node=result_mesh_node) for analysis_reference in analysis_references]) return disp
### =================================================================================================================== ### 5. End of script ### ===================================================================================================================