Source code for viiapackage.viiaGeneral

### =============================================================================================================== ###
###                                                                                                                 ###
###                                                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 viewn 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
[docs]def viia_create_cost_key_figures( project: ViiaProject, facade_list: List[Union[str, Wall]] = None, generate_excel: bool = False) -> Path: """ This function computes the cost key figures required by the cost engineer (at request of the client) and send them to MYVIIA. .. note:: Area of inner walls is not required anymore by cost engineer, August 2022. The data collected is: - BVO: Area of all the modelled floors (no fstrip and roofs), excluding the openings > 5m2, in [m2]. - BBO: Area of all the modelled ground floors, in [m2]. - BGO: Area of the facade including the openings in [m2]. - OGO: Area of the openings in the facade in [m2]. - BDO: Area of all the modelled roofs including the openings in [m2]. - ODO: Area of all the openings in the modelled roofs in [m2]. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - facade_list (list of obj): List with names or object references of the wall objects that should be considered facades of the building structure. Default value is None, the area of the facade and the opening area of the facade are not computed. - generate_excel (bool): Select to return an Excel sheet with the values for the cost engineer. A template for this Excel file is available in the viiaPackage. Default value is True. Output: - The cost key figures are calculated based on the model. - The values are sent to MYVIIA. These are uploaded if no prior values exist, or the values have changed. - The Excel with the values is created, the location of the file is returned. """ # Collect the cost key figures key_figures = viia_compute_cost_key_figures(project=project, facade_list=facade_list) # Send figures to MYVIIA response = viia_send_cost_key_figures_to_myviia(project=project, data=key_figures) # Generate excel sheet if generate_excel: return viia_cost_key_figures_to_excel(project=project, data=key_figures)
### =================================================================================================================== ### 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 ### ===================================================================================================================