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