### ===================================================================================================================
### Function to create wall displacement graphs
### ===================================================================================================================
# Copyright ©VIIA 2024
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
from __future__ import annotations
import json
from pathlib import Path
from typing import TYPE_CHECKING, Union, List, Tuple, Dict, Optional
# References for functions and classes in the rhdhv_fem package
from rhdhv_fem.analyses import Analysis
from rhdhv_fem.output_items import DisplacementOutputItem
from rhdhv_fem.shapes import Wall
from rhdhv_fem.fem_math import fem_compare_coordinates
from rhdhv_fem.fem_tools import fem_create_folder
from rhdhv_fem.fem_config import Config
# 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
from viiapackage.general.file_handling import viia_to_filename
# Import module matplotlib
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_wall_displacements
### ===================================================================================================================
[docs]def viia_wall_displacements(
project: ViiaProject, wall_oop_tbfile: Path, signal: str, wall_list: List[Union[str, Wall]] = None,
analysis: Optional[Analysis] = None) \
-> Optional[Tuple[Path, List[Path]]]:
"""
This function computes the in-plane and out-of-plane displacements for the walls through all the time steps of the
time signal.
Input:
- project (obj): Project object containing collections and of fem objects and project variables.
- wall_oop_tbfile (Path): Path to the OUTPUT_2.tb file.
- signal (str): String representing the signal, can be S1 to S11.
- wall_list (list): List of walls as object reference or wall-names as strings to be considered in this
function. Default value is None, applying all walls except for the foundation walls.
Output:
- The absolute and relative in-plane and out-of-plane displacements for each wall requested in the wall_list
(If no wall is specified then all walls are considered) are generated within the
OOP_IP_displacements folder in the working folder. The folder and a list of create files are returned in a
tuple.
"""
# Check if tb-file is present
if '_OUTPUT_2.tb' not in wall_oop_tbfile.as_posix() or not wall_oop_tbfile.exists():
project.write_log(
"WARNING: Input file not recognised for wall displacements function, check your input. No graphs are "
"generated for the wall displacements.")
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
# Read tb-file instance
project.read_diana_tbfile(file=wall_oop_tbfile, analysis=analysis)
software = 'diana'
# Check for wall selection
if wall_list is None:
wall_list = project.collections.walls
# Exclude any foundation walls
foundation_layer = project.viia_get(collection='layers', name='F')
wall_list = [wall for wall in wall_list if wall.layer is not foundation_layer]
# Find the required nodes for function
wall_names_list = list()
for i, wall in enumerate(wall_list):
for wall_object in project.collections.walls:
if 'WANDEN' in wall_object.name and 'LIJNMASSA' not in wall_object.name and \
'SPOUWANKER' not in wall_object.name:
if isinstance(wall, str) and wall_object.name == wall:
wall_names_list.append(wall_object)
elif not isinstance(wall, str) and wall_object == wall:
wall_names_list.append(wall_object)
if analysis is None and len(project.collections.analyses) != 1:
raise LookupError(
f"ERROR: Multiple instances of Analysis found={project.collections.analyses}. The wall displacements can "
f"only be made if there is one analysis in the project.")
elif analysis is None:
analysis = project.collections.analyses[0]
if not isinstance(analysis, Analysis):
raise ValueError(f"ERROR: The analysis was not properly provided. Check your input for analysis={analysis}.")
# Get results mesh nodes and output items
for output_block in analysis.get_all_output_blocks():
if output_block.name == 'OUTPUT_2':
output_items = output_block.output_items
result_mesh_nodes = output_block.manual_nodes
break
else:
raise LookupError(f"No OUTPUT_2 output block found in {analysis}.")
# Collect output items
output_item_x = None
output_item_y = None
for output_item in output_items:
if isinstance(output_item, DisplacementOutputItem):
if output_item.engineering_notation == 'U_x_tot':
if output_item_x is not None:
raise RuntimeError(
f"ERROR: Multiple displacement output-items in x-direction found in {output_block.name} of "
f"analysis {analysis}, this is not expected.")
output_item_x = output_item
elif output_item.engineering_notation == 'U_y_tot':
if output_item_y is not None:
raise RuntimeError(
f"ERROR: Multiple displacement output-items in y-direction found in {output_block.name} of "
f"analysis {analysis}, this is not expected.")
output_item_y = output_item
if output_item_x is None or output_item_y is None:
raise ValueError(
f"ERROR: Could not find the correct output-items for the wall displacement functionality in "
f"{output_block.name} of analysis {analysis}. Found values x-direction: {output_item_x} and y-direction "
f"{output_item_y}.")
# 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(
"ERROR: Multiple possible calculation_blocks found. OUTPUT_2 output is 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)
# Check if all the three nodes on the walls are in the result file output nodes
wall_nodes = []
for wall in wall_list:
temp_list = list()
nodes_to_check, coordinates_3_nodes = viia_find_3_mesh_nodes_on_wall(project=project, wall=wall)
# Find all the nodes that from result nodes that are on the wall
nodes_to_search = [mesh_node for mesh_node in wall.mesh.get_meshnodes() if mesh_node in result_mesh_nodes]
# Check if there are three evaluation nodes from the results file on the same wall
if len(nodes_to_search) != 3:
# removing un-necessary nodes from the list - 'nodes_to_search'
nodes_to_del = list()
for node in nodes_to_search:
if node not in nodes_to_check:
nodes_to_del.append(node)
for node in nodes_to_del:
nodes_to_search.remove(node)
# Check the min, middle, max nodes separately and replace with the result node if needed
for i, node in enumerate(nodes_to_check):
if node in nodes_to_search:
temp_list.append(node)
else:
# Find the closest node to the coordinate from the result nodes
closest_node = viia_find_closest_mesh_node(
project=project, target_point=coordinates_3_nodes[i],
mesh_nodes=nodes_to_search, direction='Z', precision=1)
if closest_node is None:
raise NotImplementedError(
f"ERROR: The node at the same height of as the point at coordinates {coordinates_3_nodes[i]} "
f"on {wall.name} cannot be found in the current dat-file input, please check your input for "
f"the dat-file.")
temp_list.append(closest_node)
wall_nodes.append({'wall': wall, 'nodes': temp_list})
# Get times
time_list = [analysis_reference.meta_data['time'] for analysis_reference in analysis_references]
# Create a directory
folder = project.current_analysis_folder / 'OOP_IP_displacements'
fem_create_folder(folder)
files = []
data = {}
# Collect the results
for item in wall_nodes:
wall = item['wall']
mesh_nodes = item['nodes']
nodes = dict()
z_nodes = [mesh_node.coordinates[2] for mesh_node in mesh_nodes]
if fem_compare_coordinates([z_nodes[0]], [z_nodes[1]]) or \
fem_compare_coordinates([z_nodes[0]], [z_nodes[2]]) or \
fem_compare_coordinates([z_nodes[1]], [z_nodes[2]]):
project.write_log(
f"WARNING: At least two of the three evaluation nodes {mesh_nodes} on wall '{wall.name}' are on the "
f"same z-level, please check your dat-file and evaluation nodes. Graph of wall displacements not "
f"generated.")
continue
for node in mesh_nodes:
if node.coordinates[2] == max(z_nodes):
nodes['top'] = node
elif node.coordinates[2] == min(z_nodes):
nodes['bottom'] = node
else:
nodes['center'] = node
normal_vector = wall.normal_vector()
out_of_plane_list = list()
in_plane_list = list()
interstorey_drift_perpendicular_list = list()
displacements_oop_bottom_list = list()
displacements_oop_center_list = list()
displacements_oop_top_list = list()
top_node = nodes['top']
bottom_node = nodes['bottom']
center_node = nodes['center']
for analysis_reference in analysis_references:
x_bottom = wall.results.get_result_value(
output_item=output_item_x, analysis_reference=analysis_reference, software=software,
mesh_element=None, mesh_node=bottom_node)
y_bottom = wall.results.get_result_value(
output_item=output_item_y, analysis_reference=analysis_reference, software=software,
mesh_element=None, mesh_node=bottom_node)
x_center = wall.results.get_result_value(
output_item=output_item_x, analysis_reference=analysis_reference, software=software,
mesh_element=None, mesh_node=center_node)
y_center = wall.results.get_result_value(
output_item=output_item_y, analysis_reference=analysis_reference, software=software,
mesh_element=None, mesh_node=center_node)
x_top = wall.results.get_result_value(
output_item=output_item_x, analysis_reference=analysis_reference, software=software,
mesh_element=None, mesh_node=top_node)
y_top = wall.results.get_result_value(
output_item=output_item_y, analysis_reference=analysis_reference, software=software,
mesh_element=None, mesh_node=top_node)
displacements_oop_bottom_list.append(normal_vector[0] * x_bottom + normal_vector[1] * y_bottom)
displacements_oop_center_list.append(normal_vector[0] * x_center + normal_vector[1] * y_center)
displacements_oop_top_list.append(normal_vector[0] * x_top + normal_vector[1] * y_top)
out_of_plane_list.append(
# Displacement out-of-plane at center
(normal_vector[0] * x_center + normal_vector[1] * y_center) -
# (Displacement out-of-plane at top + displacement out-of-plane at bottom) / 2
((normal_vector[0] * x_top + normal_vector[1] * y_top +
normal_vector[0] * x_bottom + normal_vector[1] * y_bottom) / 2))
in_plane_list.append(
# Displacement in-plane at top
(normal_vector[1] * x_top + normal_vector[0] * y_top) -
# Displacement in-plane at bottom
(normal_vector[1] * x_bottom + normal_vector[0] * y_bottom))
interstorey_drift_perpendicular_list.append(
# Displacement out-of-plane at top
(normal_vector[0] * x_top + normal_vector[1] * y_top) -
# Displacement out-of-plane at bottom
(normal_vector[0] * x_bottom + normal_vector[1] * y_bottom))
# Collect the data to be stored
data[wall.name] = {
'width': wall.get_width(),
'horizontal_segments': wall.get_segments(),
'height': wall.get_height(),
'thickness': wall.get_thickness(),
'displacements_oop_bottom': displacements_oop_bottom_list,
'displacements_oop_center': displacements_oop_center_list,
'displacements_oop_top': displacements_oop_top_list,
'out_of_plane_displacements': out_of_plane_list,
'interstorey_drift_perpendicular': interstorey_drift_perpendicular_list,
'interstorey_drift_parallel': in_plane_list}
# Create the graph (close existing previous ones
plt.close()
fig = plt.figure(figsize=(15, 5))
# Apply VIIA graph style
style_file = Path(project.viia_settings.project_specific_package_location) / 'viiaGraph.mplstyle'
plt.style.use(style_file.as_posix())
ax1 = plt.subplot2grid((1, 1), (0, 0))
ax1.plot(time_list, [displ * 1000 for displ in displacements_oop_bottom_list], label='Bottom')
ax1.plot(time_list, [displ * 1000 for displ in displacements_oop_center_list], label='Middle')
ax1.plot(time_list, [displ * 1000 for displ in displacements_oop_top_list], label='Top')
ax1.spines['bottom'].set_position('zero')
# ax1.annotate(labelAnnotation, xy=positionAnnotation)
plt.xlabel('Time [s]')
plt.ylabel('Displacement [mm]')
wall_name_file = viia_to_filename(wall.name)
plt.title('Wall displacement ' + str(wall.name))
plt.legend()
# Save the graph to the same folder
filename = folder / f'Wall_{wall_name_file}_total_displacements.png'
plt.savefig(filename.as_posix())
project.write_log(f"The total displacement plot was created for {wall.name}: {filename.as_posix()}.")
files.append(filename)
# Create the graph (close existing previous ones)
plt.close()
fig = plt.figure(figsize=(15, 5))
# Apply VIIA graph style
style_file = Path(project.viia_settings.project_specific_package_location) / 'viiaGraph.mplstyle'
plt.style.use(style_file.as_posix())
ax1 = plt.subplot2grid((1, 1), (0, 0))
ax1.plot(
time_list, [displ * 1000 for displ in out_of_plane_list],
label='Out-of-plane displacement')
ax1.plot(
time_list, [displ * 1000 for displ in interstorey_drift_perpendicular_list],
label='Interstorey drift, perpendicular to wall')
ax1.plot(
time_list, [displ * 1000 for displ in in_plane_list],
label='Interstorey drift, parallel to wall')
ax1.spines['bottom'].set_position('zero')
plt.xlabel('Time [s]')
plt.ylabel('Relative displacement [mm]')
plt.title('Relative displacement wall ' + str(wall.name))
plt.legend()
# Save the graph to the same folder
filename = folder / f'Wall_{wall_name_file}_relative_displacements.png'
plt.savefig(filename.as_posix())
project.write_log(f"The relative displacement plot was created for {wall.name}: {filename.as_posix()}.")
files.append(filename)
# Store the data in separate json-file
_viia_create_wall_displacements_json(project=project, data=data)
# Notify user of finishing the acceleration graphs
project.write_log(f"Wall displacement graphs for signal {signal} created in {folder.as_posix()}.")
return folder, files
### ===================================================================================================================
### 3. Function to create the wall displacements json-file for VIIA
### ===================================================================================================================
[docs]def _viia_create_wall_displacements_json(project: ViiaProject, data: Dict[str, Dict[str, List[float]]]) -> Path:
"""
Function to store the results of the wall displacements (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 wall.
Output:
- Generates a json-file in the current analysis-folder with the data of the wall displacements.
- 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 / 'wall_displacements.json'
with open(dumpfile, 'w') as fd:
json.dump(data, fd, indent=2, sort_keys=True)
return dumpfile
### ===================================================================================================================
### 4. End of script
### ===================================================================================================================