### =============================================================================================================== ###
### ###
### viiaGeneral.py ###
### ###
### =============================================================================================================== ###
# This module ``viiaGeneral`` contains general functions like functions for creation of the model, setup of the model
# and small functions that are used by all other scripts (for example coordinate handling)
# Module is based on:
# VIIA_QE_R376 Basis of Design Retrofit Advice NLTH, v11.0, d.d. 29 August 2024
# VIIA_QE_R376 Basis of Design Step-by-Step MRS, v1.0, d.d. 21 January 2022
# VIIA_QE_R1674 Uitgangspuntenrapport engineering NLPO, v1.0, d.d. 1 August 2019
# This script was based upon the Constitution For Python At VIIA
# For use by VIIA
# Copyright RHDHV
### ===================================================================================================================
### Contents
### ===================================================================================================================
# 1. Import modules
# 2. General model setup functions
# 3. Visualisation of PY-memory
# 4. Functions to find data from fem-model
# 5. Functions for actions for cost engineer
# 6. File handling (JSON, DIANA)
# 7. End of script
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General references
from __future__ import annotations
import os
import math
import copy
import json
import shutil
import re
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Optional, List, Union, Tuple, Dict, Any
from functools import lru_cache
# References for functions and classes in the rhdhv_fem package
from rhdhv_fem.fem_tools import fem_create_folder
from rhdhv_fem.fem_math import fem_compare_coordinates, fem_dot_product_vector, \
fem_centroid_of_polygon, fem_vector_2_points, fem_unit_vector, fem_compare_values, fem_distance_coordinates, \
fem_greater, fem_smaller, fem_horizontal_vector_in_plane, fem_parallel_vectors
from rhdhv_fem.materials import Masonry
from rhdhv_fem.fem_shapes import fem_min_max_points
from rhdhv_fem.shapes import Shapes, Wall, Roof, Fstrip, Floor
from rhdhv_fem.mesh import MeshNode
from rhdhv_fem.analyses import AnalysisBlock, Analysis
from rhdhv_fem.grid import Grid
from rhdhv_fem.groups import Layer
from rhdhv_fem.fem_config import Config
from rhdhv_fem.tools import fem_create_model_summary_pdf
# References for functions and classes in the viiaPackage
if TYPE_CHECKING:
from viiapackage.viiaStatus import ViiaProject
from viiapackage.general import *
# For using the viia_plot_building_cross_section function
import matplotlib
import matplotlib.pyplot as plt
### ===================================================================================================================
### 2. General model setup functions
### ===================================================================================================================
[docs]def viia_create_model(project: ViiaProject, software: str = 'VIIA'):
"""
Function selects the program to which the model must be exported or created in. For VIIA only SCIA and DIANA are
used, but other programs can be selected.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- software (str): Software in which the model is created. Default setting for VIIA is 'diana' for 'NLPO' or
'NLTH' and 'scia' for 'MRS' analysis type. In case the object needs to be created in ABAQUS software provide
'abaqus' as input (will only work for NLTH analysis objects).
Output:
- If software is 'abaqus': The model is created in ABAQUS. But only if the analysis type is NLTH.
- If software is 'diana': The model is created in DIANA, when running in DIANA.
- If software is 'scia': The xlm with model data is created and can be loaded in SCIA.
"""
# Perform the default procedure for creating the model
if software == 'VIIA':
if project.analysis_type == 'MRS':
software = 'scia'
else:
software = 'diana'
if software == 'abaqus' and project.analysis_type != 'NLTH':
raise NotImplementedError("ERROR: ABAQUS software is not available for models other than NLTH.")
project.create_model(software=software)
[docs]def viia_print_execution_time(project: ViiaProject):
"""
Function prints and logs the execution time of the MainScript (to the point where the functions is set).
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Prints the execution time and logs it.
"""
project.print_execution_time()
[docs]def viia_help():
"""
Function will open browser for additional information on viiaPackage.
Input:
- No input required.
Output:
- Default browser is opened and directed to the index page of the viiaPackage documentation.
"""
# Open the documentation in the default browser
os.startfile(r"https://viiapackage.azurewebsites.net/")
### ===================================================================================================================
### 3. Visualisation of PY-memory
### ===================================================================================================================
[docs]def viia_plot_grid(
project: ViiaProject, plot_levels: bool = True, levels_plotting_plane: Optional[str] = None,
show: bool = False, save_folder: Optional[Path] = None):
"""
This function plots all grids in the collections of the project. Optionally the levels defined in the grids are
plotted. The plotting methods for the fem package are used to create the plots.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- plot_levels (bool): Optional boolean to choose whether the levels are plotted. Default is True.
- levels_plotting_plane (str): Optional string (choose 'xz' or 'yz') to set the plotting plane for the levels.
- show (bool): Optional boolean to choose whether to show the plots. Default value is False, not showing.
- save_folder (Path): Path location to save the plots. If no path is specified, the current workfolder
is used.
Output:
- Plot of all the grids in the project is saved to the requested folder.
- Optional plot of the levels is saved to the requested folder.
"""
for grid in project.collections.grids:
project.plot_grid(grid=grid, show=show, save_folder=save_folder)
if plot_levels and grid.levels:
project.plot_levels(grid=grid, plotting_plane=levels_plotting_plane, show=show, save_folder=save_folder)
[docs]def viia_create_plots(
project: ViiaProject, collections: Optional[Union[List[Shapes], List[str]]] = None,
layers: Optional[Union[List[Layer], List[str]]] = None,
add_collections: Optional[Union[List[Shapes], List[str]]] = None, alpha: float = 0.7,
add_alpha: float = 0.7, viewpoint: Optional[List[float]] = None,
show: bool = True, title: Optional[str] = None, save_folder: Optional[Path] = None,
file_name: Optional[str] = None, use_plotly: bool = False, save_plot: bool = True, return_figure: bool = False,
legend_group_dict: Dict[Shapes, str] = None) -> Union[Optional[Path], Tuple[Optional[Path], Any]]:
"""
This function enables to create a plot of the shapes stored within the project in the Python database as
collections with an interactive GUI from Matplotlib.
Input:
- project (obj): VIIA Project object containing collections of VIIA objects and project variables.
- collections (list of obj): The list of shapes to be plotted. The list must contain references to VIIA
objects in Shapes class. Cannot be used when layers are also given.
Alternative 1 (list of str): List of string representations of shape collections to be plotted. If layers
are defined, then only shapes of the specified collections on the specified layers are plotted.
Default value is None, in which case all shapes are plotted.
- layers (list of obj): The list of layer objects for which shapes need to be plotted.
Alternative (list of str): list of layer names, the shapes of which need to be plotted
Default value is None, in which case all layers are plotted.
- add_collections (list of obj): The list of additional shapes to be plotted besides those plotted in the
specified layers. The list must contain references to VIIA objects in Shapes class.
This should be used when specific layers are given, and the additional shapes should be shapes that
are not in the specified layers or if collection is passed as a list of strings, and you want plot some
additional shapes.
Alternative 1 (list of str): List of string representations of additional shape collections to be plotted
besides those plotted in the specified layers.
Default value is None, in which case no additional shapes are plotted.
- alpha (float): Number from 0 to 1 that determines the transparency of the shapes that are plotted. Default
value is 0.7.
- add_alpha (float): Number from 0 to 1 that determines the transparency of the additional shapes that are
plotted that are defined by 'add_collections'. Default value is 0.7.
- viewpoint (list of float): List of two floats that determine the angle the plot is view from when first
created. The first entry is the elevation angle in degrees, and the second entry is the azimuth angle in
degrees. For a topview use [90, 0].
- show (bool): This is used to show the plot. By default, this is set to True. If set to False, the GUI with
the plot will not appear.
- title (str): Title to be used in picture. Default value is None, in which case the title is set based on
the shape types and layer.
- save_folder (str): Path to folder where the pictures should be saved. Default value is None, in which
case the current workfolder will be used.
- file_name (str): Name of the file to save the plot as.
- use_plotly (bool): Indicates if plotly should be used instead of matplotlib. Default is False.
- save_plot (bool): Indicates if the plot should be saved. Default is True.
- return_figure (bool): Indicates if next to the path of the save file also the picture object of matplotlib or
plotly should be returned. Default is False.
- legend_group_dict (dict): A dictionary with shape and the legend group that should be applied for it. This is
only available for plotlty plots. Default is None.
Output:
- The requested shapes are plotted in an interactive GUI.
- The path to where the saved file is returned if save_plot is True.
"""
# Function is not to be used in DIANA
if project.rhdhvDIANA.run_diana:
project.write_log("WARNING: viia_create_plots cannot be used in DIANA.")
return None
if use_plotly and not legend_group_dict:
legend_group_dict = {}
for shape in project.collections.surfaces:
if "FUNDERINGSSTROKEN" in shape.name:
legendgroup = 'FUNDERINGSSTROKEN-surface'
elif "FUNDERINGSWANDEN" in shape.name:
legendgroup = 'FUNDERINGSWANDEN-surface'
elif "F-WANDEN" in shape.name:
legendgroup = 'F-WANDEN-surface'
elif "F-STROKEN" in shape.name:
legendgroup = 'F-STROKEN-surface'
elif "B1-" in shape.name:
legendgroup = 'B1-surface'
elif "N0-" in shape.name:
legendgroup = 'N0-surface'
elif "N1-" in shape.name:
legendgroup = 'N1-surface'
elif "N2-" in shape.name:
legendgroup = 'N2-surface'
else:
legendgroup = 'other-surface'
legend_group_dict[shape] = legendgroup
for shape in project.collections.lines:
if "FUNDERING" in shape.name:
legendgroup = 'FUNDERING-lines'
elif "F-KOLOMMEN" in shape.name:
legendgroup = 'F-KOLOMMEN-lines'
elif "N0-KOLOMMEN" in shape.name:
legendgroup = 'N0-KOLOMMEN-lines'
elif "N0-BALKEN" in shape.name:
legendgroup = 'N0-BALKEN-lines'
elif "N1-KOLOMMEN" in shape.name:
legendgroup = 'N1-KOLOMMEN-lines'
elif "N1-BALKEN" in shape.name:
legendgroup = 'N1-BALKEN-lines'
elif "N2-KOLOMMEN" in shape.name:
legendgroup = 'N2-KOLOMMEN-lines'
elif "N2-BALKEN" in shape.name:
legendgroup = 'N2-BALKEN-lines'
else:
legendgroup = 'other-lines'
legend_group_dict[shape] = legendgroup
# Creating the plots
return project.create_plots(
collections=collections, add_collections=add_collections, layers=layers, show=show, alpha=alpha,
add_alpha=add_alpha, viewpoint=viewpoint, title=title, save_folder=save_folder, file_name=file_name,
use_plotly=use_plotly, save_plot=save_plot, return_figure=return_figure, legend_group_dict=legend_group_dict)
[docs]def viia_plot_layers(
project: ViiaProject, layers: Optional[Union[List[str], List[Layer], str]] = None, view: str = '2D',
shape_types: Optional[Union[str, List[str]]] = None, grid: Optional[Union[Grid, int, str]] = None,
shape_types_together: Optional[bool] = None, thickness_scale: float = 1, show: bool = True,
show_dimensions: bool = True, save_folder: Optional[Path] = None, file_name: Optional[str] = None):
"""
This function plots either all or the specified layers and all or the specified shape types.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- layers (list of obj): List of layer objects to plot. Default value is None, in which case all the layers are
plotted.
Alternative 1 (list of obj): List of strings representing the layers to plot.
Alternative 2 (str): String representing one layer to plot.
- view (str): Optional string to indicate whether layer(s) should be plotted in 2D or 3D. Default value is '2D'.
- shape_types (str): String indicating the shape type to plot. Default value is None, in which case all
walls, floors, roofs, beams and columns are plotted
- grid (Grid, int, str): Optional object, id or name of the grid to use. By default, the first grid in the
collection is used.
- shape_types_together (bool): If True, all shape_types defined are plotted together in the same plot. If False,
a plot is generated separately for each shape type.
- thickness_scale (float): A scaling factor for the thickness of the plotted walls. Default value is 2.
- show (bool): True if plots will be shown, otherwise they are just saved in the given folder. Default
value is True.
- show_dimensions (bool): True if dimensions should be plotted. Default value is True.
- save_folder (Path): Path to folder to save plots in. If not provided, plots are saved in sub folder
'Modelling Pictures' in working folder.
- file_name (str): Name of the file to save the plot as. If not provided a name will be generated based on layer
name and shape-types.
Output:
- All specified or default layers and shapes are plotted and shown if desired.
- All plots made are saved in the specified or default folder.
"""
# Create folder to save pictures
if not save_folder:
save_folder = project.workfolder_location / 'Modelling Pictures'
fem_create_folder(save_folder)
# Get the grid
grid_obj = project.find_grid(grid_description=grid)
# Check the given layers
all_layers = project.collections.layers
layers_plot = []
if not layers:
layers_plot = all_layers
elif isinstance(layers, str):
layers_plot = [project.find(layers, 'layers')]
elif all(isinstance(layer, str) for layer in layers):
layers_plot = [project.find(layer, 'layers') for layer in layers]
elif not all(isinstance(layer, Layer) for layer in layers):
raise ValueError(
f'Input for layers is invalid. Must be a list with all strings, list with all layer objects, or a string.')
for i, layer in enumerate(layers_plot):
if not layer:
raise ValueError(f'{layers[i]} could not be found. Has it been created?')
# Check the given shape types
if not shape_types:
shape_types = ['walls', 'floors', 'roofs', 'beams', 'columns']
elif isinstance(shape_types, str):
shape_types = [shape_types]
if view == '2D':
for shape_type in shape_types:
if not hasattr(project.collections, shape_type):
raise ValueError(f'Shape type \'{shape_type}\' is not valid.')
elif view == '3D':
for shape_type in shape_types:
if not hasattr(layers_plot[0], shape_type):
raise ValueError(f'Shape type \'{shape_type}\' is not valid.')
# Default value of shape_types_together is based on the view
if not shape_types_together:
if view == '2D':
shape_types_together = False
elif view == '3D':
shape_types_together = True
# Plot the layers
if view == '2D':
for layer in layers_plot:
if view == '2D':
if not shape_types_together:
for shape_type in shape_types:
layer.plot(
grid=grid_obj, shape_types=shape_type, thickness_scale=thickness_scale, show=show,
show_dimensions=show_dimensions, save_folder=save_folder, file_name=file_name)
else:
layer.plot(
grid=grid_obj, shape_types=shape_types, thickness_scale=thickness_scale, show=show,
show_dimensions=show_dimensions, save_folder=save_folder, file_name=file_name)
else:
if not shape_types_together:
for shape_type in shape_types:
project.create_plots(
collections=[shape_type], layers=layers_plot, show=show, save_folder=save_folder,
file_name=file_name)
else:
project.create_plots(
collections=shape_types, layers=layers_plot, show=show, save_folder=save_folder, file_name=file_name)
[docs]def viia_plot_building_cross_section(
project: ViiaProject, plotting_plane: str = 'xz', grid: Optional[Union[Grid, int, str]] = None,
show: bool = False, title: Optional[str] = None, save_folder: Optional[Path] = None,
file_name: Optional[str] = None):
"""
This function plots all shapes and the given grid from the specified plane.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- plotting_plane (str): Optional argument to specify a plane for the plot. Two options are available: 'xz'
and 'yz'.
- grid (Grid, int, str): Optional object, id or name of the grid to use. By default, the first grid in the
collection is used.
- show (bool): True if plots will be shown, otherwise they are just saved in the given folder. Default
value is True.
- title (str): Title to be used in picture. Default value is None, in which case the title is set based on
the plotting plane.
- save_folder (Path): Path to folder to save plots in.
- file_name (str): Name of the file to save the plot as.
Output:
- The specified cross-section is plotted and shown if desired.
- The picture is saved in the specified or default location.
"""
# Variables used for plotting
matplotlib.use(Config.MPL_GUI())
# Create folder to save pictures
if not save_folder:
save_folder = Path.cwd() / 'Modelling Pictures'
fem_create_folder(save_folder)
# Get the grid
grid_obj = project.find_grid(grid_description=grid)
# Plot the levels in the foreground and fetch axes and figures.
grid_obj.plot_levels(plotting_plane=plotting_plane.strip('+').strip('-'), show=False, alpha=0.4, keep_plot=True)
# Get axes
ax1 = plt.gca()
# Set the plotting range based on the coordinates in the collection of shapes
plot_range = fem_min_max_points(collection=project.collections.shapes)
x_dom = plot_range['x-max'] - plot_range['x-min']
y_dom = plot_range['y-max'] - plot_range['y-min']
# Set x and y indices for plotting based on plane
if 'xz' in plotting_plane:
x_ind = 0
y_ind = 2
elif 'yz' in plotting_plane:
x_ind = 1
y_ind = 2
else:
raise ValueError(f'Viewpoint must be in either xz- or yz-planes.')
# Plot all surfaces
for shape in project.collections.surfaces:
x = []
y = []
for point in shape.contour.get_points():
x.append(point[x_ind])
y.append(point[y_ind])
# If a shape is found to be completely perpendicular to the plotting plane, the openings will not be plotted
plot_openings = True
if 'xz' in plotting_plane:
x1 = shape.contour.get_max_x()
x2 = shape.contour.get_min_x()
if fem_compare_values(x1, x2):
plot_openings = False
elif 'yz' in plotting_plane:
y1 = shape.contour.get_max_y()
y2 = shape.contour.get_min_y()
if fem_compare_values(y1, y2):
plot_openings = False
# Set zorder based on the plotting plane direction provided
zorder = 5
if '+xz' in plotting_plane or plotting_plane == 'xz':
y_min = shape.contour.get_min_y() - plot_range['y-min']
zorder = 0 if y_min == 0 else y_dom / y_min
elif '-xz' in plotting_plane:
y_max = shape.contour.get_max_y() - plot_range['y-min']
zorder = 0 if y_max == 0 else y_max / y_dom
elif '+yz' in plotting_plane or plotting_plane == 'yz':
x_min = shape.contour.get_min_x() - plot_range['x-min']
zorder = 0 if x_min == 0 else x_dom / x_min
elif '-yz' in plotting_plane:
x_max = shape.contour.get_max_x() - plot_range['x-min']
zorder = 0 if x_max == 0 else x_max / x_dom
ax1.fill(x, y, zorder=zorder, facecolor='y', linewidth=0.8, edgecolor='k')
if shape.openings and plot_openings:
for opening in shape.openings:
x_op = []
y_op = []
for point in opening.get_points():
x_op.append(point[x_ind])
y_op.append(point[y_ind])
ax1.fill(x_op, y_op, zorder=zorder, facecolor='w', linewidth=1, edgecolor='k', alpha=0.8)
# Invert x-axis if looking towards positive y or negative z
if plotting_plane in ['yz', '+yz', '-xz']:
ax1.invert_xaxis()
# Set title
if title is None:
title = f'Cross Section {plotting_plane}'
plt.title(title, fontsize=16)
# Save figure
if not file_name:
plt.savefig(save_folder / f'{title}.png', bbox_inches='tight', format='png')
else:
plt.savefig(save_folder / f'{file_name}.png', bbox_inches='tight', format='png')
# Show the plot if desired
if show:
plt.show()
plt.close()
### ===================================================================================================================
### 4. Functions to find data from fem-model
### ===================================================================================================================
[docs]def _viia_find_floor_mesh_nodes(project: ViiaProject) -> Dict:
"""
This function finds all the nodes within the floors of the model.
.. warning:: The model must be meshed, the dat-file should have been created and the dat-file should have been
read and converted to DATDict, the dictionary containing the information in the dat-file.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Returns a dictionary with storey numbers (N0, N1, etc.) as keys with lists of corresponding floor node numbers
of that storey as values.
"""
# Initiate dictionary
floor_node_dictionary = dict()
# Get horizontal floors, i.e. excluding inclined roof shapes
floors = [floor for floor in project.collections.floors if floor.contour.is_horizontal()]
# Collect the nodes per floor
for floor in floors:
storey = project.viia_get_level(shape=floor)
if storey not in floor_node_dictionary:
floor_node_dictionary[storey] = []
floor_node_dictionary[storey] += floor.mesh.get_meshnodes()
# Return dictionary with nodes per storey
return floor_node_dictionary
[docs]def _viia_find_nodes_top_ridge(project: ViiaProject, tolerance=0.001) -> List[MeshNode]:
"""
This function collects the top mesh-nodes (in z-coordinate) within floor and roof shapes. The maximum z-coordinate
is determined and any nodes within the tolerated distance from this maximum plane are collected.
.. warning:: The model must be meshed and read into the project. For diana this is done with project.from_diana.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- tolerance (float): the tolerated distance from the found maximum z-coordinate
Output:
- Returns a list with MeshNode objects.
"""
if len(project.collections.roofs) > 0:
coord = fem_min_max_points(project.collections.roofs)
coordinates_goal = [
(coord['x-max'] + coord['x-min']) / 2,
(coord['y-max'] + coord['y-min']) / 2,
coord['z-max']]
top_node = viia_find_closest_mesh_node(project=project, target_point=coordinates_goal, direction='Z')
else:
# This is the case roofs are modelled as floors
coord = fem_min_max_points(project.collections.floors)
coordinates_goal = [
(coord['x-max'] + coord['x-min']) / 2,
(coord['y-max'] + coord['y-min']) / 2,
coord['z-max']]
top_node = viia_find_closest_mesh_node(project=project, target_point=coordinates_goal, direction='Z')
nodes_top_ridge = []
for node in project.collections.mesh_nodes:
if abs(node.coordinates[2] - top_node.coordinates[2]) < \
tolerance:
nodes_top_ridge.append(node)
return nodes_top_ridge
[docs]def _viia_find_foundation_nodes_under_wall(
project: ViiaProject, node_fstrip_list: List[int], wall_name: str) -> Union[List[int], None]:
"""
This function will look for the closest node of the foundation strips in the mesh to the point in the bottom
line of the given wall. The z-coordinate of the bottom line is determined by the smallest z-coordinate of the
contour of the wall. The function will loop through the nodes on foundation strip to check whether the nodes are
under this wall.
.. warning:: The model must be meshed, the dat-file should have been created and the dat-file should have been
read and converted to DATDict, the dictionary containing the information in the dat-file.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- node_fstrip_list (list of int): A list of the all the node numbers in the foundation strips
- wall_name(str): Name of the requested wall.
Output:
- Returns a list with multiple nodes (integers).
"""
node_list = []
try:
elements = project.diana_settings.dat_file.elementsets[wall_name]['elements']
except KeyError:
return None
for element in elements:
for i, node in enumerate(project.diana_settings.dat_file.elements[element]['nodes']):
node_list.append(int(node))
node_list = list(set(node_list))
# Find min and max coordinates of wall
xmax = -1000
xmin = 1000
ymax = -1000
ymin = 1000
zmax = -1000
zmin = 1000
for node in node_list:
if fem_smaller(xmax, project.diana_settings.dat_file.nodes[node][0]):
xmax = project.diana_settings.dat_file.nodes[node][0]
if fem_greater(xmin, project.diana_settings.dat_file.nodes[node][0]):
xmin = project.diana_settings.dat_file.nodes[node][0]
if fem_smaller(ymax, project.diana_settings.dat_file.nodes[node][1]):
ymax = project.diana_settings.dat_file.nodes[node][1]
if fem_greater(ymin, project.diana_settings.dat_file.nodes[node][1]):
ymin = project.diana_settings.dat_file.nodes[node][1]
if fem_smaller(zmax, project.diana_settings.dat_file.nodes[node][2]):
zmax = project.diana_settings.dat_file.nodes[node][2]
if fem_greater(zmin, project.diana_settings.dat_file.nodes[node][2]):
zmin = project.diana_settings.dat_file.nodes[node][2]
# Determine the x- or y-coordinate by the direction of this wall
x_direction = False
y_direction = False
x_line = 0
y_line = 0
if fem_smaller(abs(ymax - ymin), 10 ** (-project.check_precision0)):
y_line = ymax
y_direction = True
elif fem_smaller(abs(xmax - xmin), 10 ** (-project.check_precision)):
x_line = xmax
x_direction = True
# Target points
goal_node = []
# Determine the closest node in the nodelist
if x_direction:
minimum_distance = 1E8
for node in node_fstrip_list:
distance = math.sqrt(math.pow(project.diana_settings.dat_file.nodes[node][0] - x_line, 2))
if fem_smaller(distance, minimum_distance):
minimum_distance = distance
for node in node_fstrip_list:
distance = math.sqrt(math.pow(project.diana_settings.dat_file.nodes[node][0] - x_line, 2))
if fem_smaller(abs(distance - minimum_distance), 10 ** (-project.check_precision)):
goal_node.append(node)
elif y_direction:
minimum_distance = 1E8
for node in node_fstrip_list:
distance = math.sqrt(math.pow(project.diana_settings.dat_file.nodes[node][1] - y_line, 2))
if fem_smaller(distance, minimum_distance):
minimum_distance = distance
for node in node_fstrip_list:
distance = math.sqrt(math.pow(project.diana_settings.dat_file.nodes[node][1] - y_line, 2))
if fem_smaller(abs(distance - minimum_distance), 10 ** (-project.check_precision)):
goal_node.append(node)
return goal_node
[docs]def _viia_find_wall_groups_for_nlka_in_model(project: ViiaProject) -> Dict:
"""
This function will look for all the walls in the model, group them with the required parameters in the NLKA
assessment procedure.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- A dictionary is returned, which contains all the wall groups and all required parameters for nlka assessment.
It also includes all the wall names in the group.
"""
def materiaal_name_define(name_of_wall):
"""
Translate the material name to the names in the Excel tool.
"""
if 'MW-KLEI<1945' in name_of_wall:
return 'Metselwerk, klei voor 1945'
elif 'MW-KLEI>1945' in name_of_wall:
return 'Metselwerk, klei na 1945'
elif 'MW-KZS>1960' in name_of_wall:
return 'Metselwerk, kzst 1960-1985'
elif 'MW-KZS>1985' in name_of_wall:
return 'Metselwerk, kzst na 1985'
elif 'MW-PORISO' in name_of_wall:
return 'Metselwerk, poriso'
# Set the name as the keys for the whole dictionary
group_name = 'Groep_'
dict_of_all_groups = {}
# Set empty value for parameter dictionary already for each group
dict_parameters = {
'Wand dikte muur': None,
'Dikte spouw': None,
'Dikte spouwblaad': None,
'Materiaal': None,
'Wand hoogte': None,
'Excentriciteit boven': None,
'Excentriciteit onder': None,
'Richting': None,
'Z-coordinaat van de hart van de wand': None,
'Type van de berekende elementen': None,
'Z-coordinaat van het terrein ten opzichte van SCIA model': None,
'list_of_walls': []}
# Set the start number for groups
group_num = 1
# Remove the foundation walls from the loop
wall_lst = []
for wall in project.collections.walls:
if 'FUNDERING' not in wall.name and isinstance(wall.material, Masonry):
wall_lst.append(wall)
# Start the group compare
for wall in wall_lst:
temp_dict = copy.deepcopy(dict_parameters)
temp_dict['Wand dikte muur'] = round(wall.geometry.geometry_model.thickness * 1e3, 0)
wall_material = wall.material.name
temp_dict['Materiaal'] = materiaal_name_define(wall_material)
temp_dict['Wand hoogte'] = round(wall.contour.get_height(), 1)
# Find the boundary condition of the top connecting floor
upper_floor_lst = _viia_find_upper_floor_of_wall(wall)
# Check if the wall is inner wall or not
if viia_check_inner_wall(wall):
# Judge the boundary condition now by material of the floor
for floor in upper_floor_lst:
if (not (not ('HOUT' in floor.material.name) and not ('HBV' in floor.material.name) and not (
'HSB' in floor.material.name) and not ('NEHOBO' in floor.material.name))) and (
'DAK' not in floor.name):
temp_dict['Excentriciteit boven'] = 2
elif 'BETON' in floor.material.name:
temp_dict['Excentriciteit boven'] = 1
else:
temp_dict['Excentriciteit boven'] = floor.material.name
else:
if upper_floor_lst:
for floor in upper_floor_lst:
if (not (not ('HOUT' in floor.material.name) and not ('HBV' in floor.material.name) and not (
'HSB' in floor.material.name) and not ('NEHOBO' in floor.material.name))) and (
'DAK' not in floor.name):
if abs(fem_dot_product_vector(
floor.element_x_axis.vector, fem_unit_vector(wall.normal_vector()))) == 1:
temp_dict['Excentriciteit boven'] = 2
elif fem_dot_product_vector(
floor.element_x_axis.vector, fem_unit_vector(wall.normal_vector())) == 0:
temp_dict['Excentriciteit boven'] = 'Not in the upper floor span direction'
elif 'BETON' in floor.material.name:
temp_dict['Excentriciteit boven'] = 1
else:
temp_dict['Excentriciteit boven'] = floor.material.name
else:
temp_dict['Excentriciteit boven'] = 'Cantilever'
# Find the boundary condition of the top connecting floor
lower_floor_lst = _viia_find_lower_floor_of_wall(wall)
# Judge the boundary condition now by material of the floor
for floor in lower_floor_lst:
if 'N0' in wall.name:
f_floor = _viia_find_foundation_strip_below_wall(wall)
if f_floor:
if 'MW' in f_floor.name:
temp_dict['Excentriciteit onder'] = 5
elif 'BETON' in f_floor.name:
temp_dict['Excentriciteit onder'] = 4
else:
temp_dict['Excentriciteit onder'] = f_floor.material.name
else:
temp_dict['Excentriciteit onder'] = 'Fstrip not found for N0_wall'
else:
if ('HOUT' in floor.material.name or 'HBV' in floor.material.name or 'HSB' in floor.material.name or
'NEHOBO' in floor.material.name) and ('DAK' not in floor.name):
temp_dict['Excentriciteit onder'] = 5
elif 'BETON' in floor.material.name:
temp_dict['Excentriciteit onder'] = 4
else:
temp_dict['Excentriciteit onder'] = floor.material.name
# Find the direction of the wall
if [round(abs(i), 1) for i in wall.normal_vector()] == [0, 1.0, 0]:
temp_dict['Richting'] = 'X'
elif [round(abs(i), 1) for i in wall.normal_vector()] == [1.0, 0, 0]:
temp_dict['Richting'] = 'Y'
# Find the z coordinate of the center of the wall
temp_dict['Z-coordinaat van de hart van de wand'] = round(viia_find_wall_center_z_coordinate(wall), 1)
# The type of the element
temp_dict['Type van de berekende elementen'] = 'PSE'
fstrip_temp = _viia_find_foundation_strip_below_wall(wall)
if fstrip_temp:
temp_dict['Z-coordinaat van het terrein ten opzichte van SCIA model'] = \
fem_centroid_of_polygon(fstrip_temp.contour.get_points())[2]
else:
temp_dict['Z-coordinaat van het terrein ten opzichte van SCIA model'] = None
if not dict_of_all_groups.keys():
dict_of_all_groups[group_name + str(group_num)] = copy.deepcopy(temp_dict)
dict_of_all_groups[group_name + str(group_num)]['list_of_walls'].append(wall.name)
else:
flag_new_group = False
for group, parameter in dict_of_all_groups.items():
# Set the flag to see if compare needs to be continued or not
flag_new_group = False
keys = list(dict_of_all_groups[group].keys())
keys.remove('list_of_walls')
for key in keys:
compared_value = dict_of_all_groups[group][key]
temp_dict_value = temp_dict[key]
if compared_value != temp_dict_value:
flag_new_group = True
break
if not flag_new_group:
dict_of_all_groups[group]['list_of_walls'].append(wall.name)
break
if flag_new_group:
group_num += 1
dict_of_all_groups[group_name + str(group_num)] = copy.deepcopy(temp_dict)
dict_of_all_groups[group_name + str(group_num)]['list_of_walls'].append(wall.name)
return dict_of_all_groups
[docs]def viia_find_wall_center_z_coordinate(wall: Wall) -> float:
"""
This function will get the z coordinate of the center of the wall by using the fem_centroid_of_polygon function.
Input:
- wall (obj): Object reference of Wall object.
Output:
- Returns a float of the z-coordinate of the center of the wall.
"""
return fem_centroid_of_polygon(wall.contour.get_points())[2]
[docs]def _viia_find_intersecting_floors_for_wall(project: ViiaProject, wall):
"""
This function will get the intersecting floors of a wall by the following two steps: First check if both points of
a line of the wall are in the surface of the floor. Else check if both points of a line of the floor are in the
surface of the wall.
.. note:: Make sure the shapes are properly connected using the connect_shape function.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- wall: Wall object.
Output:
- Returns a list of the intersecting floors of the wall.
"""
list_floors = []
for shape in wall.get_connecting_shapes():
if isinstance(shape, Floor):
floor = shape
found = False
for line_wall in wall.contour.lines:
# Check if both nodes of the line are on the surface of the floor
condition1 = floor.is_point_in_shape(line_wall.node_start.coordinates)
condition2 = floor.is_point_in_shape(line_wall.node_end.coordinates)
if condition1 and condition2 and not isinstance(floor, Roof):
list_floors.append(floor)
found = True
break
if found:
pass
else:
for line_floor in floor.contour.lines:
# Check if both nodes of the line are on the surface of the floor
condition1 = wall.is_point_in_shape(line_floor.node_start.coordinates)
condition2 = wall.is_point_in_shape(line_floor.node_end.coordinates)
if condition1 and condition2 and not isinstance(floor, Roof):
list_floors.append(floor)
break
return list_floors
[docs]def _viia_find_upper_floor_of_wall(wall: Wall) -> List[Floor]:
"""
This function will get the upper intersecting floors of a wall by using the function of
viia_find_intersecting_floors_for_wall, the method here to judge if it is an upper floor is to compare the floor
z_coordinate with the center of the wall coordinate.
Input:
- wall (obj): Wall object.
Output:
- Returns a list of the upper intersecting floors of the wall.
"""
project = wall.project
floor_lst = _viia_find_intersecting_floors_for_wall(project, wall)
upper_tem = []
for floor in floor_lst:
if fem_greater(floor.contour.lines[0].node_start.coordinates[2], viia_find_wall_center_z_coordinate(wall)):
upper_tem.append(floor)
return upper_tem
[docs]def _viia_find_lower_floor_of_wall(wall):
"""
This function will get the lower intersecting floors of a wall by using the function of
viia_find_intersecting_floors_for_wall, the method here to judge if it is a lower floor is to compare the floor
z_coordinate with the center of the wall coordinate.
Input:
- wall: Wall object.
Output:
- Returns a list of the lower intersecting floors of the wall.
"""
project = wall.project
floor_lst = _viia_find_intersecting_floors_for_wall(project, wall)
lower_tem = []
for floor in floor_lst:
if fem_smaller(floor.contour.lines[0].node_start.coordinates[2], viia_find_wall_center_z_coordinate(wall)):
lower_tem.append(floor)
return lower_tem
[docs]def _viia_find_foundation_strip_below_wall(wall) -> Optional[Fstrip]:
"""
This function will get the underlying foundation strip of a certain wall by check if the intersection exists from
the center of the wall pointing to the foundation strip.
Input:
- wall (obj): Wall object.
Output:
- Returns the foundation strip object below the wall.
"""
project = wall.project
for strip in project.collections.fstrips:
point_on_wall = list(fem_centroid_of_polygon(wall.contour.get_points()))
if strip.is_intersecting(point=point_on_wall, direction=[0, 0, -1]):
return strip
project.write_log(f"The underlying foundation is not found for {wall.name}.")
return None
def _viia_find_top_wall(project: ViiaProject, wall: Wall):
"""
Finds the top wall connected to the given wall in the project.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- wall: The wall to find the top wall for.
Output:
- The top wall if found, otherwise None.
"""
wall_center_z = viia_find_wall_center_z_coordinate(wall)
if wall is None:
return None
def lines_match(line1, line2):
return (fem_compare_coordinates(line1.node_start.coordinates, line2.node_start.coordinates) and
fem_compare_coordinates(line1.node_end.coordinates, line2.node_end.coordinates)) or \
(fem_compare_coordinates(line1.node_start.coordinates, line2.node_end.coordinates) and
fem_compare_coordinates(line1.node_end.coordinates, line2.node_start.coordinates))
for shape in wall.get_connecting_shapes():
if isinstance(shape, Wall):
for line_wall in wall.contour.lines:
if line_wall.is_horizontal():
for line_top_wall in shape.contour.lines:
if (lines_match(line_wall, line_top_wall) and
fem_greater(line_top_wall.node_start.coordinates[2], wall_center_z)):
return shape
return None
[docs]def viia_get_flat_roofs(project: ViiaProject) -> List[Roof]:
"""
This function collects all the flat roofs. It loops only through the roof object collection and checks if the
normal vector of the contour of the roof is vertical.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- List with all the object references of flat roofs.
"""
return [roof for roof in project.collections.roofs if roof.contour.is_horizontal()]
[docs]def viia_check_inner_wall(wall: Wall) -> bool:
"""
This function will check if a wall is an inner wall. This is assessed in two steps:
- First check if the start node of any line of the wall is inside the surface of the floor, or if the coordinate of
middle of the top line of the wall is inside the floor surface when both top nodes of the wall are on the contour
of the floor. If true then it is an inner wall, if not then it goes to step 2.
- Second check if the wall has upper floors on its both sides, if true then it is an inner wall, otherwise it is not
an inner wall.
.. note:: Before applying this function make sure to connect all shapes first.
Input:
- wall (obj): Object reference of Wall object.
Output:
- Returns a boolean whether the wall is an inner wall or not.
"""
# Find all the floors connecting to the wall
floor_lst = _viia_find_upper_floor_of_wall(wall)
# First judge if the wall is inside the surface of the floors
for floor in floor_lst:
for line_wall in wall.contour.lines:
# Check if both nodes of the line are inside the surface of the floor but not on the contour of the surface
condition1 = floor.is_point_in_shape(line_wall.node_start.coordinates)
condition2 = floor.contour.is_point_on_contour(line_wall.node_start)
condition3 = floor.is_point_in_shape(line_wall.node_end.coordinates)
condition4 = floor.contour.is_point_on_contour(line_wall.node_end)
if (condition1 and not condition2) or (condition3 and not condition4):
# At least one node is inside the surface of the floor
return True
# Check if the middle of the top line of the wall is inside the floor surface and not on contour if both
# nodes are on the contour of the floor
if condition2 and condition4:
# Here condition can also be 'on_node_of_condition', so True is used here
middle_point = [(line_wall.node_start.coordinates[i] +
line_wall.node_end.coordinates[i]) / 2 for i in range(3)]
if floor.is_point_in_shape(middle_point) and not floor.contour.is_point_on_contour(middle_point):
return True
# Check if there are floors on the both side of the wall
vector_lst = []
dot_product_lst = []
normal_vector_wall = wall.normal_vector()
# Find the center of wall and all the floors
center_of_floor_lst = []
center_of_wall = list(fem_centroid_of_polygon(wall.contour.get_points()))
for floor in floor_lst:
center_of_floor_lst.append(list(fem_centroid_of_polygon(floor.contour.get_points())))
# Create list of vectors from wall center to centers of floors
for coordinate in center_of_floor_lst:
vector_lst.append(fem_vector_2_points(center_of_wall, coordinate))
# Calculate the dot products of normal vector and each vector from vector list
for vector in vector_lst:
dot_product_lst.append(fem_dot_product_vector(normal_vector_wall, vector))
# Check if there are positive and negative dot product in the lst
negative = False
positive = False
for dot_product in dot_product_lst:
if dot_product < 0:
negative = True
if dot_product > 0:
positive = True
if negative and positive:
return True
else:
return False
[docs]def viia_get_element_weight(dat_file, element):
"""
This function will calculate the weight of an element by multiplying its volume by its density.
.. warning:: The model must be meshed, the dat-file should have been created and the dat-file should have been
read and converted to DATDict, the dictionary containing the information in the dat-file.
Input:
- dat: dat file of the model.
- element: element number
Output:
- Returns the weight of the element in the defined unit in dat file.
"""
def find_the_elementset(elementsets, num_element):
for elementset in elementsets.keys():
elements_lst = elementsets[elementset]['elements']
if num_element in elements_lst:
return elementsets[elementset]
def get_area_for_line(geometry_dict):
total_area = 0
if 'ISHAPE' in geometry_dict.keys():
shape_lst = geometry_dict['ISHAPE']
total_area += shape_lst[1] * shape_lst[3] + shape_lst[2] * shape_lst[4]
total_area += (shape_lst[0] - shape_lst[3] - shape_lst[4]) * shape_lst[5]
return total_area
elif 'RECTAN' in geometry_dict.keys():
total_area += geometry_dict['RECTAN'][0] * geometry_dict['RECTAN'][1]
return total_area
else:
print(f"WARNING: The area of {element} is not calculated, so the weight calculation can be incorrect.")
# Get the area of the element
area = dat_file.elements[element]['area']
# Get the thickness and density of the element
num_geometry = find_the_elementset(dat_file.elementsets, element)['geometry']
num_material = find_the_elementset(dat_file.elementsets, element)['material']
thickness = 0
density = 0
length = 0
for geometry in dat_file.geometries.keys():
if dat_file.geometries[geometry]['number'] == num_geometry:
try:
thickness = dat_file.geometries[geometry]['THICK']
except KeyError:
node_start = dat_file.elements[element]['nodes'][0]
node_end = dat_file.elements[element]['nodes'][1]
length = math.sqrt(math.pow(dat_file.nodes[node_start][0] - dat_file.nodes[node_end][0], 2) +
math.pow(dat_file.nodes[node_start][1] - dat_file.nodes[node_end][1], 2) +
math.pow(dat_file.nodes[node_start][2] - dat_file.nodes[node_end][2], 2))
area = get_area_for_line(dat_file.geometries[geometry])
break
for material in dat_file.materials.keys():
if dat_file.materials[material]['number'] == num_material:
density = dat_file.materials[material]['DENSIT']
break
# Calculate the weight of the element
if thickness == 0:
weight = length * density * area
else:
weight = area * thickness * density
return weight
[docs]def _viia_get_weight_of_elements_in_box(project: ViiaProject, x_boundary, y_boundary, z_boundary):
"""
This function will calculate the weight of all the elements in a box by summing the weight of all elements.
.. warning:: The model must be meshed, the dat-file should have been created and the dat-file should have been
read and converted to DATDict, the dictionary containing the information in the dat-file.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- dat: dat file of the model.
- x_boundary: list containing the x-boundaries of the box [start-coordinate, end-coordinate]
- y_boundary: list containing the y-boundaries of the box [start-coordinate, end-coordinate]
- z_boundary: list containing the z-boundaries of the box [start-coordinate, end-coordinate]
Output:
- Returns the weight of all the elements in the defined unit in dat file.
"""
element_list = []
nodes_dict = project.diana_settings.dat_file.nodes
# To check if the element is inside that box
for element in project.diana_settings.dat_file.elements:
in_flag = True
for node in project.diana_settings.dat_file.elements[element]['nodes']:
if (not (not (nodes_dict[node][0] < x_boundary[0]) and not (nodes_dict[node][0] > x_boundary[1]) and not (
nodes_dict[node][1] < y_boundary[0]) and not (nodes_dict[node][1] > y_boundary[1]) and not (
nodes_dict[node][2] < z_boundary[0])) or nodes_dict[node][2] > z_boundary[1]):
in_flag = False
if in_flag:
element_list.append(element)
break
else:
break
# Calculate the total weight
total_weight = 0
for item in element_list:
total_weight += viia_get_element_weight(project.diana_settings.dat_file, item)
return total_weight
[docs]def viia_find_closest_mesh_node(
project: ViiaProject, target_point: List[float], mesh_nodes: List[MeshNode] = None, direction: str = None,
precision: int = None, shape: Shapes = None) -> MeshNode:
"""
Function to find the closest mesh node in the latest mesh.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- target_point (list with 3 floats): Target point to find the closest node for.
- mesh_nodes (list of obj. ref.): List of MeshNodes to check the shortest distance for. Default value None, all
mesh-nodes stored in the collections will be used.
- direction (str): Option to search the closest node on a certain plane. The closest
node returned has the same coordinate, depending on this input. Default set to 'None', no direction. Options
are 'X', 'Y' and 'Z'. Input 'Z' will return a node with the same z-coordinate as the target point. If no such
a point exists, None is returned.
- precision (float): In case the direction is given, the precision is used to tuning how exact the node should
be in coordinate of X, Y or Z directions. Default value is the project check-precision.
- shape (obj): Object of shape class. If this input is given then the closest node will be found from
within the nodes of this shape only. Default value is None.
Output:
- If node_number is selected as return_type, the closest node is returned as number in the
project.diana_settings.dat_file node collection. If set to complete, a list of MeshNode object,
node coordinates and distance to target point is returned. Else the MeshNode object is returned.
"""
if mesh_nodes is None:
if shape is not None:
if shape.mesh is None:
raise ValueError(f"ERROR: Shape should have a mesh if it is used as input. Check shape {shape.name}")
mesh_nodes = shape.mesh.get_meshnodes()
else:
mesh_nodes = project.collections.mesh_nodes
if not mesh_nodes or not isinstance(mesh_nodes, list):
raise LookupError("ERROR: No MeshNodes were found.")
# Check for precision
if precision is None:
precision = project.check_precision
# Filter nodes for direction:
if direction:
if direction.lower() == 'x':
index = 0
elif direction.lower() == 'y':
index = 1
elif direction.lower() == 'z':
index = 2
else:
raise ValueError(
f"ERROR: viia_find_closest_mesh_node is unable to consider direction {direction}, acceptable "
f"values are 'X', 'Y', and 'Z'.")
mesh_node_dict = \
{mesh_node: [mesh_node.coordinates[index]] for mesh_node in mesh_nodes if
fem_compare_values(value1=mesh_node.coordinates[index], value2=target_point[index], precision=precision)}
if len(mesh_node_dict) == 0:
raise ValueError(
f"ERROR: viia_find_closest_mesh_node is not able to find nodes on the same {direction}-plane as target "
f"point {target_point}. Function is aborted.")
target_point = [target_point[index]]
else:
mesh_node_dict = {mesh_node: mesh_node.coordinates for mesh_node in mesh_nodes}
# Initiate node distance list
mesh_node_distance_list = []
# Loop over nodes and compute distance to target point
for mesh_node, mesh_node_coordinates in mesh_node_dict.items():
# Compute distance to target
distance_to_target = fem_distance_coordinates(coordinate1=mesh_node_coordinates, coordinate2=target_point)
# Append to list
mesh_node_distance_list.append([mesh_node, mesh_node_coordinates, distance_to_target])
# Sort the list if no exact match is found
mesh_node_distance_list = sorted(mesh_node_distance_list, key=lambda x: abs(x[2]))
# Select closest node
return mesh_node_distance_list[0][0]
[docs]def _viia_foundation_area(project: ViiaProject):
"""
This function calculates the area of the supported surfaces in DIANA.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Returns the area of supported surfaces as float.
- Adds the information in the logfile.
"""
area_py = 0
area_diana = 0
for shape in project.viia_supported_surfaces:
if shape in project.collections.lines:
project.write_log(
f"ERROR: Supported surface '{str(shape.name)}' is a line-support, area can not be calculated with "
f"viia_foundation_area function.")
elif shape in project.collections.surfaces:
area = shape.contour.get_area()
for opening in shape.openings:
area -= opening.get_area()
area_py += area
if project.rhdhvDIANA.run_diana:
area_diana += project.rhdhvDIANA.areaOf(shape.name)
else:
project.write_log(
f"ERROR: Supported surface '{str(shape.name)}' is of unknown support-type, area can not be "
f"calculated with viia_foundation_area function.")
if project.rhdhvDIANA.run_diana:
if not fem_compare_coordinates([area_py], [area_diana], project.check_precision):
project.write_log(
"WARNING: There is a difference between the calculated area of the supported surfaces in py-"
"memory and the area of the supported surfaces in DIANA. Please check. The value of the area"
"in DIANA is used for further processing.")
project.write_log(
f"Total area of shallow foundation found in model is {str(area_diana)} m2.")
return area_diana
return area_py
### ===================================================================================================================
### 5. Functions for actions for cost engineer
### ===================================================================================================================
[docs]def viia_get_inner_walls(project: ViiaProject):
"""
Function returns and prints list of inner walls, based on VIIA functions. This function should be used to check
and if required manually update list to use in consecutive functions for cost engineer. The function makes use
of the viia_check_inner_wall functionality.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Returns and prints the inner wall shapes as list.
"""
inner_wall_list = []
for wall in project.collections.walls:
if not wall.name.startswith('FUNDERING') and not wall.name.startswith('F-') and not wall.name.endswith('B'):
if viia_check_inner_wall(wall):
inner_wall_list.append(wall)
# Notification
project.write_log(
f"The following wall shapes are expected to be inner walls, please verify:\n"
f" You can use this list to send to the cost engineer of VIIA. "
f"Add any deviation manually in your mainscript.\n"
f" inner_wall_list = {[wall.name for wall in inner_wall_list]}.")
return inner_wall_list
[docs]def viia_get_facades(project: ViiaProject):
"""
Function returns and prints list of facade walls, based on VIIA functions. This function should be used to check
and if required manually update list to use in consecutive functions for cost engineer. The function makes use
of the viia_check_inner_wall functionality.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Returns and prints the facade shapes as list.
"""
facade_list = []
for wall in project.collections.walls:
if not wall.name.startswith('FUNDERING') and not wall.name.startswith('F-') and not wall.name.endswith('B'):
if not viia_check_inner_wall(wall):
facade_list.append(wall)
# Notification
project.write_log(
f"The following wall shapes are expected to be facades, please verify:\n You can use this "
f"list to send to the cost engineer of VIIA. Add any deviation manually in your mainscript.\n"
f" facade_list = [{', '.join([wall.name for wall in facade_list])}].")
return facade_list
### ===================================================================================================================
### 6. File handling (JSON, DIANA) and loading models
### ===================================================================================================================
[docs]def viia_write_dump(
project: ViiaProject, filename: Optional[str] = None, folder: Optional[Path] = None,
create_summary: bool = True, result_json_only: bool = False)\
-> Tuple[Optional[Path], Optional[Path], Optional[Path]]:
"""
This function writes a dump-file of the current PY-memory. It saves the shapes, its attributes, Project data,
properties, etc. The function viia_read_dump can be applied to reload.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- filename (str): The name of the json-file to be written, default value is None, creating a filename based on
the projectname and extend with '-model'.
- folder (str or path): The name of the sub-folder where the file is to be written can be given, folder should
be available. The sub-folder given in the argument is relative to and in the working directory. If not
provided the json will be saved to the 'model' folder that is a sub-folder in the working folder.
- create_summary (bool): Indicates if a pdf with a model summary should be made. Default is True.
- result_json_only (bool): Switch to indicate only the result json should be generated. Only works if results=
True, and return_result_json=True. By default, the switch is set to False.
Output:
- Dumpfile is created by default in sub-folder 'model' of working directory or in given sub-folder of the
working directory. The file contains the current state of the model in PY-memory.
- Based on the model a summary is created.
- A tuple is returned with the location of the model json-file, result json-file and model summary pdf-file.
"""
# Check filename
if not filename:
filename = f"{project.name}-model.json"
filename = str(filename)
if '.json' not in filename:
filename += '.json'
# Select the sub-folder
if not folder:
folder = Path.cwd() / 'model'
if isinstance(folder, str):
folder = Path.cwd() / folder
fem_create_folder(folder)
dumpfile = folder / filename
# Function in rhdhv_fem package for writing the json
project.write_log("Starting to create json-file of the FEM-model.")
file = project.write_dump(file=dumpfile, return_result_json=True, result_json_only=result_json_only)
if isinstance(file, tuple): # when result are present both model and result json will be made
model_json = file[0]
result_json = file[1]
else:
model_json = file
result_json = None
# Create model summary if required
model_summary = None
base_layer = project.viia_get("layers", name='N0')
if create_summary and not result_json_only:
model_summary = fem_create_model_summary_pdf(
project=project, json=model_json, output_folder=folder, base_area=base_layer)
# Return path
return model_json, result_json, model_summary
[docs]def viia_read_dump(project: ViiaProject, filename='Dump', remove_collections=True, loading_model: str = 'all', *args):
"""
Function can be used to reload a model created earlier.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- filename (Path or str): Name of the file to be read. It is not required to add the suffix (json), but the
file must have the json extension.
- remove_collections (bool): Before adding new elements to the model, the existing ones are removed by default.
Default value is True.
- loading_model (str): Defines the categories to be loaded, different sets are created, increasing the amount
of elements to be loaded. Default value is 'all', collecting all data from the json-file.
- args (str): Any sub-folders relative to the working folder can be added.
Output:
- The json is read and objects in it are initialised.
"""
# Function in rhdhv_fem package for reading the json
if args:
project.read_dump(file_name=filename, remove_collections=remove_collections, loading_model=loading_model, *args)
else:
project.read_dump(file_name=filename, remove_collections=remove_collections, loading_model=loading_model)
# Check for directions
if not project.viia_get('directions', name='X'):
project.create_direction(vector=[1.0, 0.0, 0.0], name='X')
if not project.viia_get('directions', name='-X'):
project.create_direction(vector=[-1.0, 0.0, 0.0], name='-X')
if not project.viia_get('directions', name='Y'):
project.create_direction(vector=[0.0, 1.0, 0.0], name='Y')
if not project.viia_get('directions', name='-Y'):
project.create_direction(vector=[0.0, -1.0, 0.0], name='-Y')
# Check for grid
if not project.collections.grids:
grid_x_coord = list()
grid_y_coord = list()
# Grid layout based on walls (Only walls parallel to x-axis and y-axis are considered to create the grid)
for wall in project.collections.walls:
wall_horizontal_axis = fem_horizontal_vector_in_plane(wall.contour.get_points(), project.check_precision)
if fem_parallel_vectors(wall_horizontal_axis, [1.0, 0, 0], True, project.check_precision):
# Wall is parallel to x-axis
grid_y_coord.append(wall.contour.get_points()[0][1])
elif fem_parallel_vectors(wall_horizontal_axis, [0, 1.0, 0], True, project.check_precision):
# Wall parallel to y axis
grid_x_coord.append(wall.contour.get_points()[0][0])
# Sorting and making elements unique
grid_x_coord = sorted(list(set(grid_x_coord)))
grid_y_coord = sorted(list(set(grid_y_coord)))
# Grid creation
project.viia_create_orthogonal_grid([grid_x_coord, grid_y_coord])
# Check for levels
if not project.collections.levels:
levels = list()
# Create the initial level at z=0.0m
levels.append(0.0)
# Add floor levels
for floor in project.collections.floors:
z_coord = floor.contour.get_points()[0][2]
# Check if the distance between z_coord and any element in levels is less than 0.1m.
if all(abs(z_coord - level) >= 0.1 for level in levels):
levels.append(z_coord)
# Add roof levels
if project.collections.roofs:
max_z_values = \
[max(abs(node[2]) for node in roof.contour.get_points()) for roof in project.collections.roofs]
# Highest roof level
absolute_greatest_z = max(max_z_values)
levels.append(absolute_greatest_z)
# Check if the distance between max_z and any element in levels is less than 0.1m.
for max_z in max_z_values:
if all(abs(max_z - level) >= 0.1 for level in levels):
levels.append(max_z)
# Make levels unique and in ascending order
levels = sorted(list(set(levels)))
project.viia_create_levels(z_coordinates= levels)
# Set the ID following the naming convention
for shape in project.collections.shapes:
if 'PAAL#' in shape.name: # Get id from shape with names like: PAAL#21_TYPE_A
id_found = re.search(r"#([0-9]+)", shape.name)
if id_found is not None:
shape.id = int(id_found.group(1))
else:
raise ValueError(
f"ERROR: ID of shape {shape.name} could not be determined. ID of 'PAAL'-shape was not found, "
f"please check if the naming convention PAAL#21_TYPE_A is applied.")
else:
# Get id from shape with names like: N2-BALKEN-LIN-HOUT-D100-179
shape_id = shape.name.split('-')[-1].replace('In', '').replace('Out', '').replace('B', '').replace(
'_b', '').replace('b', '')
if shape_id.isnumeric() and '.' not in shape_id and "," not in shape_id:
shape.id = int(shape_id)
else:
error = shape.name.split('-')[-1].replace('In', '').replace('Out', '').replace('B', '').replace(
'b', '')
raise ValueError(
f"ERROR: ID of shape {shape.name} could not be determined. String [%s] cannot be converted "
f"to integer." % error)
# Cavity walls are numbered in 9000 series
if shape.name[-1].upper() == 'B':
shape.id += 9000
# Check for layer
if shape.layer is None:
name_layer_part = shape.name.split('-')[0]
if name_layer_part in [
'FUNDERINGSWANDEN', 'FUNDERINGSSTROKEN', 'FUNDERINGSBALKEN', 'FUNDERINGSKOLOMMEN',
'FUNDERINGSVLOEREN', 'PAAL', 'FUNDERINGSKOLOM', 'FUNDERINGS-LIJNMASSA',
'FUNDERINGSBUITENSPOUWBLADEN'] or 'PAAL' in name_layer_part:
layer = project.find(description='F', collection='layers')
if layer is None:
layer = project.viia_create_layer(name='F')
layer.add_shape(shape)
else:
layer = project.find(description=name_layer_part, collection='layers')
if layer is None:
try:
layer = project.viia_create_layer(name=shape.name.split('-')[0])
except ValueError:
project.write_log(
f"WARNING: The layer for shape {shape} could not be retrieved. This might result in an "
f"error.")
if layer:
layer.add_shape(shape)
# Set object references in meta-data for VIIA specific input
for fstrip in project.collections.fstrips:
if fstrip.meta_data and 'foundation_wall' in fstrip.meta_data:
fstrip.meta_data['foundation_wall'] = \
project.find(description=fstrip.meta_data['foundation_wall'], collection='walls')
elif fstrip.meta_data and 'equiv_dimensions' in fstrip.meta_data:
if 'normal_wall' in fstrip.meta_data['equiv_dimensions']:
fstrip.meta_data['equiv_dimensions']['normal_wall'] = \
project.find(description=fstrip.meta_data['equiv_dimensions']['normal_wall'], collection='walls')
if 'equiv_wall' in fstrip.meta_data['equiv_dimensions']:
fstrip.meta_data['equiv_dimensions']['equiv_wall'] = \
project.find(description=fstrip.meta_data['equiv_dimensions']['equiv_wall'], collection='walls')
if 'cavity_extension_wall' in fstrip.meta_data['equiv_dimensions']:
fstrip.meta_data['equiv_dimensions']['cavity_extension_wall'] = \
project.find(
description=fstrip.meta_data['equiv_dimensions']['cavity_extension_wall'], collection='walls')
if fstrip.meta_data and 'supported_wall' in fstrip.meta_data:
fstrip.meta_data['supported_wall'] = \
project.find(description=fstrip.meta_data['supported_wall'], collection='walls')
if fstrip.meta_data and 'foundation_cavity_wall' in fstrip.meta_data:
fstrip.meta_data['foundation_cavity_wall'] = \
project.find(description=fstrip.meta_data['foundation_cavity_wall'], collection='walls')
# Check and correct the NPR response spectrum
for spectrum in project.collections.response_spectra:
spectrum.importance_factor = project.importance_factor
if 'cluster_nen' in project.project_information:
spectrum.cluster = project.project_information['cluster_nen']
# Check for grid information for beams and collar ties on grid
for beam in project.collections.beams:
if beam.meta_data and 'grid' in beam.meta_data:
beam.meta_data['grid'] = project.viia_get(collection='grids', unique_id=beam.meta_data['grid'])
# Check for roof-beams information stored in the meta-data
for beam in project.collections.beams:
if beam.meta_data and 'roof_beams' in beam.meta_data:
project.project_specific['lists']['roof_beams'].append(beam)
for connection in project.collections.connections:
# Check for layer
for name_part in ['FUNDERINGSWANDEN', 'FUNDERINGSSTROKEN', 'FUNDERING']:
if name_part in connection.name:
connection.name = connection.name.replace(name_part, 'F')
# Check for consistency
for collection in ['floors', 'walls', 'columns', 'beams']:
collection_list = []
if hasattr(project.collections, collection):
collection_list = getattr(project.collections, collection)
collect_unique_ids = []
first_warning = True
for shape in collection_list:
checker = False
for item in collect_unique_ids:
if shape.id == item.id:
checker = True
if first_warning:
project.write_log(
f"WARNING: Inconsistency found in numbering of {collection} in the model. "
f"You can proceed with caution, some functions may yield unwanted results. Consider "
f"updating your json first.")
first_warning = False
project.write_log(
f"WARNING: Shape {shape.name} has same ID ({shape.id}) as {item.name}. Consider updating your "
f"json first.")
break
if not checker:
collect_unique_ids.append(shape)
[docs]def viia_read_myviia_model(project: ViiaProject, analysis: str = 'A1') -> Dict[str, Any]:
"""
This function will collect the model from MYVIIA that was uploaded for a specified analysis. The function will raise
an error when there is already a model present, or if something went wrong in the process to collect or read.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- analysis (str): Name of the analysis for which the model should be loaded, for example 'A1'.
Output:
- The model is read from MYVIIA and loaded into project.
- Returns model summary of the loaded json.
"""
if analysis == 'A1':
return viia_read_myviia_a1_model(project=project)
raise NotImplementedError(
f"ERROR: Analysis {analysis} is not yet available on MYVIIA. Currently you can only load a model from MYVIIA "
f"for analysis A1.")
[docs]def _viia_analysis_diana_gui_files(project: ViiaProject, analysis: Analysis):
"""
When running an analysis directly from script in the DIANA GUI, the calculation files are located in the working
directory. For result handling, the files need to be moved to the appropriate analysis folder. This function
performs the moving and deleting of files.
When running a calculation in DIANA it creates a result file (dnb-file), a filos file (ff-file) and out files
(out-files). The filos file is not needed anymore and is removed with this function. The result and out files are
moved to the current analysis folder (project.current_analysis_folder). There functions for result handling will
look for the results.
.. note:: After running a calculation in DIANA the result file is loaded in DIANA GUI automatically. When directly
running this function, the file is locked and a copy is placed in the analysis folder. The result file in
working directory can be removed after closing DIANA. But it is better to run picture scripts etc. directly
after running the calculation and then remove the analysis in the script (the file is then unlocked).
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- analysis (obj): Object reference of 'Analysis' class. The analysis which will be performed.
Default value is 'None'. In case of SCIA calculations no analysis is needed. In case of DIANA calculations,
the analysis it is required to provide the analysis.
Alternative (str): Name of the analysis to be performed (there should be an object with that analysis).
Output:
- Files are moved and deleted.
- Notifications are given of errors.
- Returns the out-file.
"""
# Argument handling
if analysis and type(analysis) == str:
for analysis_obj in project.collections.analyses:
if analysis_obj.name == analysis:
analysis = analysis_obj.name
break
if analysis and analysis not in project.collections.analyses:
project.write_log(f"Moving files from DIANA GUI analysis aborted: '{analysis}' not recognised.")
return None
# Files to be moved and removed are located in working directory
working_folder = project.workfolder_location
# Name and location of the out-file when running in GUI is
outfile_to_move = working_folder / f"{project.name}_v{project.version}_{analysis.name.replace(' ', '_')}.out"
if not outfile_to_move.is_file():
outfile_to_move = working_folder / f"{analysis.name.replace(' ', '_')}.out"
# Name and location of the result-file when running in GUI is
resultfiles_to_move = []
for i, calculation_block in enumerate(analysis.calculation_blocks):
if isinstance(calculation_block, AnalysisBlock):
for j, output_block in enumerate(calculation_block.output_blocks):
if output_block.output_device is not None:
if output_block.output_device.lower() == 'tabulated' and \
working_folder / str(output_block.output_filename + '.tb') not in resultfiles_to_move:
resultfiles_to_move.append(working_folder / str(output_block.output_filename + '.tb'))
else:
if working_folder / str(output_block.output_filename + '.dnb') not in resultfiles_to_move:
resultfiles_to_move.append(working_folder / str(output_block.output_filename + '.dnb'))
# Unload the results
project.rhdhvDIANA.unloadResults(analysis.name, resultfiles_to_move[-1].as_posix())
def move_file(file_to_move, logging_name):
"""
Function moves the files and logs any problems.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- file (str): Name and location of the file (full path).
- logging_name (str): Name of the file as shown in logging and on screen.
"""
logging_name_cap = logging_name[0].upper() + logging_name[1:]
moved_file = project.current_analysis_folder / file_to_move.name
if file_to_move.exists():
file_to_move = file_to_move.as_posix()
try:
moved_file = shutil.move(file_to_move, project.current_analysis_folder)
project.write_log(f"{logging_name_cap} of {analysis.name} moved to analysis folder.")
project.write_log(f"{logging_name_cap}: {file_to_move}.", False)
project.write_log(
f"Analysisfolder: {project.current_analysis_folder.as_posix()}.", False)
return Path(moved_file)
except PermissionError:
project.write_log(
f"{logging_name_cap} of {analysis.name} is locked, a copy is made in analysis folder.")
project.write_log(f"{logging_name_cap} of {analysis.name} moved to analysis folder.")
project.write_log(f"{logging_name_cap}: {file_to_move}.", False)
project.write_log(
f"Analysisfolder: {project.current_analysis_folder.as_posix()}.", False)
return Path(moved_file)
except shutil.Error:
project.write_log(f"{logging_name_cap} of {analysis.name} already present, file will be overwritten.")
file_to_delete = file_to_move.replace(working_folder.as_posix() +
'\\', project.current_analysis_folder.as_posix())
check_bool = True
try:
os.unlink(file_to_delete)
except PermissionError:
project.write_log(
f"{logging_name_cap} of {analysis.name} is locked, and cannot be removed from the analysis "
f"folder. New {logging_name} is not moved, and is still located in working directory.")
check_bool = False
if check_bool:
try:
moved_file = shutil.move(file_to_move, project.current_analysis_folder)
return Path(moved_file)
except PermissionError:
project.write_log(
f"{logging_name_cap} of {analysis.name} is locked. New {logging_name} is not moved, "
f"and is still located in working directory.")
return Path(moved_file)
except FileNotFoundError:
project.write_log(f"{logging_name_cap} of {analysis.name} not found, please check.")
project.write_log(f"{logging_name_cap} name and location expected to be: {file_to_move}.")
return Path(moved_file)
else:
project.write_log(f"{logging_name_cap} of {analysis.name} not found, please check.")
project.write_log(f"{logging_name_cap} name and location expected to be: {file_to_move.as_posix()}.")
return moved_file
# Move the out file
moved_outfile = move_file(file_to_move=outfile_to_move, logging_name='out-file')
# Move the result file
for resultfile_to_move in resultfiles_to_move:
if analysis.name.split(' - ')[0] == 'A2':
moved_outfile = move_file(file_to_move=resultfile_to_move, logging_name='result-file')
else:
move_file(file_to_move=resultfile_to_move, logging_name='result-file')
# Delete the filos file (deletes all in the working folder)
for file in project.workfolder_location.iterdir():
if file.is_file() and file.suffix == '.ff':
try:
file.unlink()
except PermissionError:
project.write_log(
f"WARNING: Filos-file of {file.name} is locked, and is not removed from working directory.")
else:
if file.is_file():
project.write_log(
f"WARNING: The Filos-file of {file.name} could not be removed from working directory.")
else:
project.write_log(
f"Filos-file of {file.name} removed from working directory.")
return moved_outfile
[docs]def viia_get_signal(project: ViiaProject, signal) -> Optional[Tuple]:
"""
This function will return four lists containing the data from the input signals from the VIIA-Database-Signals.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- signal (str): Name of the signal, for example 'S1'.
Output:
- Returns 4 lists as tuple, containing respectively: time-signal, accelerations in x-direction, accelerations
in y-direction and accelerations in z-direction.
"""
if signal not in ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7']:
project.write_log(
f"ERROR: Input argument for signal not recognised '{signal}', please correct input. "
f"Function viia_get_signal aborted.")
return None
# Create the name of signals as used in the database
signal = signal.replace('S', 'Signal')
# Open the database from the viiaPackage
signals_file = project.viia_settings.project_specific_package_location / 'VIIA-Database-Signals.json'
with open(signals_file) as json_file:
json_data = json.load(json_file)
# Return the required signal
return \
json_data[0]['Time' + signal], \
json_data[0]['FactorX' + signal], \
json_data[0]['FactorY' + signal], \
json_data[0]['FactorZ' + signal]
[docs]@lru_cache(maxsize=None)
def _viia_get_material_database(project: ViiaProject):
"""
This function will return the json data from the VIIA-Database-Materials. As a function with lru_cache decorator,
once the same input project is provided, the cached data will be returned instead of running through this function.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Return de json data of the database.
"""
# Open the database from the viiaPackage
materials_file = project.viia_settings.project_specific_package_location / 'VIIA-Database-Materials.json'
with open(materials_file) as json_file:
json_data = json.load(json_file)
return json_data
[docs]def viia_get_material(project: ViiaProject, material_name: str, material_group: str) -> Optional[Dict]:
"""
This function will return a dictionary with the material properties from the VIIA-Database-Materials.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- material_name (str): Name of the material that is looked up in the material database.
- material_group (str): Name of the group to which the material belongs.
Output:
- Returns dictionary with the properties of the material from the database.
"""
# Get the database from the viiaPackage
json_data = _viia_get_material_database(project)
# Return the required material properties dictionary
if material_group in json_data[0]:
if material_name in json_data[0][material_group]:
material_data_dict = copy.deepcopy(json_data[0][material_group][material_name])
# Booleans for handling analysis type logic
has_analysis_specific = 'analysis_specific' in material_data_dict.keys()
has_analysis = has_analysis_specific and project.analysis_type in material_data_dict['analysis_specific']
has_nlth = has_analysis_specific and 'NLTH' in material_data_dict['analysis_specific']
# Warn the user if the material properties for the specified analysis type are not available in the database
# Update to include analysis type-specific properties if the analysis type is specified in the database
# If the analysis type is None, use the analysis type-specific properties of NLTH as default
if project.analysis_type is not None and not has_analysis:
warnings.warn(
f"{material_name} material properties for {project.analysis_type} not in materials database.")
elif has_analysis:
material_data_dict.update(material_data_dict['analysis_specific'][project.analysis_type])
elif project.analysis_type is None and has_nlth:
material_data_dict.update(material_data_dict['analysis_specific']['NLTH'])
# Delete analysis-type specific data
if has_analysis_specific:
del material_data_dict['analysis_specific']
return material_data_dict
return None
[docs]def viia_check_materialname_present(project: ViiaProject, material_name) -> Union[None, str]:
"""
This function checks if the material is present in the material-database of VIIA.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- material_name (str): Name of the material that is looked up in the material database.
Output:
- Returns the name of the material group to which the looked up material belongs. If not found 'None' will be
returned.
"""
# Get the database from the viiaPackage
json_data = _viia_get_material_database(project)
# Return correct material group for materials in database whose material group depends on analysis type
sbs_linear_isotropic = ["LIN-COMBI-DATO-170-40", "LIN-COMBI-VBI-170-30", "LIN-COMBI-VBI-170-40"]
if material_name in sbs_linear_isotropic:
if project.analysis_type == 'SBS':
return 'Linear'
else:
return 'LinearOrthotropic'
# Check if material name is in dict
for material_group in json_data[0]:
for material_name_key in json_data[0][material_group]:
if material_name == material_name_key:
return material_group
# If material not found return None
return None
### ===================================================================================================================
### 7. End of script
### ===================================================================================================================