Source code for viiapackage.results.result_functions.viia_limits

### ===================================================================================================================
###   Functionality for finding analysis-type-specific checks and corresponding limit values for shapes
### ===================================================================================================================
# Copyright ©VIIA 2024

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

# General imports
from __future__ import annotations
from typing import TYPE_CHECKING, Dict, Tuple, Optional, List
import re
import yaml
import warnings
from copy import deepcopy
from functools import lru_cache

# References for functions and classes in the rhdhv_fem package
from rhdhv_fem.fem_math import fem_compare_values, fem_parallel_vectors, fem_length_vector, fem_vector_2_points, \
    fem_smaller
from rhdhv_fem.shapes import Shapes, Surfaces, Wall, Floor, Roof, Reinforcements
from rhdhv_fem.groups import Layer

# References for functions and classes in the viiaPackage
if TYPE_CHECKING:
    from viiapackage.viiaStatus import ViiaProject


### ===================================================================================================================
###   2. Helper functions
### ===================================================================================================================

[docs]@lru_cache(maxsize=None) def _viia_get_database_limits(project: ViiaProject) -> Optional[Dict]: """ This function will return a dictionary with the limit values for analysis-type-specific checks from the VIIA-LimitValues-Databases. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. Output: - Return dictionary with the limit values for analysis-type-specific checks from the VIIA-LimitValues-Databases. """ # Read yaml with VIIA limit values with open(project.viia_settings.project_specific_package_location / 'results' / f'result_pictures_limits_library_{project.analysis_type}.yaml') as f: return yaml.load(f, Loader=yaml.FullLoader)
[docs]@lru_cache(maxsize=None) def _viia_find_limits_material_data(material_name: str) -> Optional[Tuple[str, str, str]]: """ This function will return a tuple with the generic material, a first iteration of the material name for searching in the VIIA-LimitValues-Databases, and the linearity for the material name passed. Input: - material_name (str): The name of a material according to VIIA naming convention. Output: - Return tuple with generic material, first iteration of material name for searching in database, and linearity. """ # Dictionary with keywords that are associated with generic materials material_keywords = { 'Concrete': ['BETON', 'KPV', 'HVBP-DURISOL', 'MPV', 'COMBI-DATO', 'COMBI-VBI', 'RIB-FLEVO'], 'Steel': ['STAAL', 'STDAK', 'COSTEEL'], 'Timber': ['HOUT', 'HOUT-LAM', 'SANDWICH', 'HBV-PLANKEN', 'HBV-PLATEN', 'UNIDEK', 'HSB'], 'Masonry': ['MW-AAC', 'MW-KLEI', 'MW-KZS', 'MW-PORISO', 'NEHOBO'], 'Reinforcement': ['WAP']} # Associate passed material name to keywords dictionary, find return values, and return them if they are found material_keywords = {key: sorted(val, key=len, reverse=True) for key, val in material_keywords.items()} for generic, materials in material_keywords.items(): for keyword in materials: keyword_match = re.search(rf'(LIN-)?{keyword}', material_name) if keyword_match: if keyword_match.group(1): linearity = 'Linear' else: linearity = 'Nonlinear' generic_material = generic material = keyword_match.group() break else: continue break else: return return generic_material, material, linearity
[docs]def _viia_find_interstorey_height(project: ViiaProject, shape: Shapes, layers: List[Layer]) -> Optional[float]: """ This function will return the interstorey height for the passed shape. Only the passed layers are considered for determining this value. Input: - project (obj): Project object containing collections and of fem objects and project variables. - shape (obj): Shape for which the interstorey height is computed. - layers (list): List containing the layers used to determine the interstorey height Output: - Interstorey height for the passed shape. """ # Find the interstorey height depending on the type of the passed shape interstorey_height = None if isinstance(shape, Wall): # Warn the user about the drift height calculation method if the shape of the wall is unconventional if not shape.contour.get_top_horizontal_lines() or not shape.contour.get_bottom_horizontal_lines(): warnings.warn( f"WARNING: Either the top or bottom parts of the wall contour of wall {shape.name} are not horizontal. " "The bottom- and top-most points of the top- and bottom-most edges, respectively, are used to " "determine the interstorey height.") sorted_lines = sorted([line for line in shape.contour.get_lines()], key=lambda x: x.get_center_point()[2]) interstorey_height = sorted_lines[-1].get_min_z() - sorted_lines[0].get_max_z() return interstorey_height
[docs]def _viia_find_effective_height( project: ViiaProject, shape: Shapes, layers: List[Layer]) -> Optional[Tuple[float, float]]: """ This function will return the effective height for the passed shape and the number of storeys of the building. Only the passed layers are considered for determining these values. Input: - project (obj): Project object containing collections and of fem objects and project variables. - shape (obj): Shape for which the effective height is computed. - layers (list): List containing the layers used to determine the effective height Output: - Effective height for the passed shape and number of storeys of the building. """ # Find the number of storeys of the building storey_num = len(layers) # If the number of storeys is neither 1 nor 2, warn the user and act accordingly if storey_num == 0: warnings.warn( f"WARNING: The building has no storeys. Make sure you are using the correct layer naming convention. The " f"effective height drift check for {shape} is not performed.") return elif storey_num > 2: warnings.warn( f"WARNING: The building has {storey_num} storeys. The in-plane drift limit for 2-storey buildings is used. " "Discuss with your lead engineer.") storey_num = 2 # Find the building height and corresponding top floor/roof top_shape, building_height = max( [[s, s.get_center_of_mass()[0]['z']] for s in project.collections.floors + project.collections.roofs], key=lambda x: x[1]) # Warn the user if the building height could not be determined or if the top floor/roof is inclined. Act accordingly if fem_smaller(building_height, 0): warnings.warn( "WARNING: The building height could not be determined because the center of mass of the top floor/roof is " "not greater than 0.") return elif not fem_compare_values(top_shape.contour.get_min_z(), top_shape.contour.get_max_z()): warnings.warn( f"WARNING: Shape {top_shape}, which determines the building height, is inclined. Its center of mass is " f"used as the building's height.") # Find the transformation factor for the shape if storey_num == 1: t_factor = 1.0 else: # storey_num == 2 warnings.warn( "WARNING: A transformation factor of 1.0 is used for computing the effective height of this 2-storey " "building. This a conservative approximation.") t_factor = 1.0 # CODE FOR MORE PRECISE CALCULATION OF t_factor BELOW. MAY BE USED IN THE FUTURE # DRIFTS DICTIONARY WILL BE DEFINED LATER # drifts = {} # layer_masses = {layer.name: layer.get_mass() for layer in layers} # modal_mass = sum(layer_masses[layer.name] * drifts[layer.name] ** 2 for layer in layers) # t_factor = layer_masses[shape.layer.name] * drifts[shape.layer.name] / modal_mass # Find the effective height of the shape and return it along with the number of storeys return building_height / t_factor, storey_num
### =================================================================================================================== ### 3. Get limits ### ===================================================================================================================
[docs]def _viia_find_strength_limits_nlth( shape: Shapes, material: str, thicknesses: List[float], strength_class: str, reduced_limits_database: Dict) -> Optional[Dict]: """ This function returns a dictionary with the strength checks and corresponding limit values to be performed on a shape for NLTH. Checks and limits are retrieved from a reduced form of the VIIA-LimitValues-NLTH-Database. This function is called by a higher-level function that determines the input arguments. Input: - shape (obj): Shape for which checks and limit values are retrieved. - material (str): Material of the shape as it appears in the database. - thicknesses(list): Nominal thicknesses of the material and its top layer (if applicable). - strength_class (str): Strength class of the material. - reduced_limits_database (Dict): Reduced version of the VIIA-LimitValues-NLTH-Database. Output: - Returns a dictionary with the checks as keys and the corresponding limit values as values. """ # Dictionary to return, with checks as keys and limit values as values limits = {} # Populate dictionary with data for the specific checks (i.e. data for the specific strength quantities) for specific_check, limit in reduced_limits_database.items(): # Populate dictionary depending on how data is represented in the database if isinstance(limit, dict) and 'Thickness' in limit.keys(): # Find the thickness from which the distributed limit force is calculated thickness = 0 if 'Varied' == limit['Thickness']: # For the joint thickness 20mm is used thickness += thicknesses[0] - 0.02 else: if 'Floor' in limit['Thickness']: thickness += thicknesses[0] if 'TopLayer' in limit['Thickness']: thickness += thicknesses[-1] limits['N' + specific_check[1:]] = limit['Limit'] * thickness else: # If the limit depends on the material strength if isinstance(limit, dict): if strength_class in limit.keys(): limit = limit[strength_class] # Warn user if limit for strength class is not available in the database and assign default value else: warnings.warn( f"WARNING: Limit value for strength class {strength_class[1:]} not available in database. " f"Limit value for default strength class of {material[:-len(strength_class)]} is used instead.") limit = limit['Default'] # Populate distributed force limit for Surfaces shapes and stress limits for other shapes if isinstance(shape, Surfaces) and not isinstance(shape, Reinforcements) and \ re.search(r'^[ft]_[a-z]*$', specific_check): limits['N' + specific_check[1:]] = limit * thicknesses[0] else: limits[specific_check] = limit # Return limits dictionary return limits
[docs]def _viia_find_drift_limits_nlth( shape: Shapes, interstorey_height: float, eff_height: float, storey_num: float, thicknesses: List[float], ductility: str, support: str, reduced_limits_database: Dict) -> Optional[Dict]: """ This function returns a dictionary with the drift checks and corresponding limit values to be performed on a shape for NLTH. Checks and limits are retrieved from a reduced form of the VIIA-LimitValues-NLTH-Database. This function is called by a higher-level function that determines the input arguments. Input: - shape (obj): Shape for which checks and limit values are retrieved. - interstorey_height (float): Interstorey height of the shape. - eff_height (float): Material of the shape as it appears in the database. - storey_num (float): Number of storeys of the building. - thicknesses (list): Nominal thicknesses of the material and its top layer (if applicable). - ductility (str): Ductility class to which the failure mode of the building corresponds. - support (str): Support conditions of the shape. - reduced_limits_database (Dict): Reduced version of the VIIA-LimitValues-NLTH-Database. Output: - Returns a dictionary with the checks as keys and the corresponding limit values as values. """ # Dictionary to return, with checks as keys and limit values as values limits = {} # Populate dictionary with data for In-plane specific check if 'In-plane' in reduced_limits_database.keys(): # If value for 'In-plane' key is a float, check is for floors/roofs, else check is for walls if isinstance(reduced_limits_database['In-plane'], float): # Find shape direction shape_dir = shape.x_axis_direction().vector # Find spans of all lines parallel to shape parallel_lines_span = [ fem_length_vector(vector=fem_vector_2_points(*line.get_points())) for line in shape.contour.get_lines() if fem_parallel_vectors( vector_1=fem_vector_2_points(*line.get_points()), vector_2=shape_dir, precision=shape.project.check_precision)] # Find drift limit if the shape has contour lines parallel to its x-axis if not parallel_lines_span: warnings.warn( f"WARNING: Shape {shape.name} has no contour lines that are parallel to its x-axis. Therefore its " f"span and drift limit are not computed.") else: # Define the shape span based on the ratio of the shortest to the longest span of the x-axis-parallel # lines if fem_smaller(min(parallel_lines_span) / max(parallel_lines_span), 0.5): warnings.warn( f"WARNING: The shortest to longest ratio between the contour lines of shape {shape} that are " f"parallel to its x-axis is very small. Therefore the length of the longest of these lines is " f"taken as the shape's span.") shape_span = max(parallel_lines_span) else: shape_span = min(parallel_lines_span) # Find drift limit and populate dictionary limits['In-plane'] = reduced_limits_database['In-plane'] * shape_span else: # Sub-dictionary with information for specific drift heights height_diff = reduced_limits_database['In-plane'][ductility] # Find the drift limits drift_limits = [] if 'Interstorey' in height_diff.keys(): drift_limits.append(height_diff['Interstorey'] * interstorey_height) if eff_height and 'EffHeight' in height_diff.keys() and f'{storey_num}-storey' in height_diff[ 'EffHeight'].keys(): drift_limits.append(height_diff['EffHeight'][f'{storey_num}-storey'] * eff_height) # Populate dictionary with minimum drift limit if drift_limits: limits['In-plane'] = min(drift_limits) # Populate dictionary with data for Out-of-plane specific check if thicknesses and 'Out-of-plane' in reduced_limits_database.keys() and 'Interstorey' in reduced_limits_database[ 'Out-of-plane'].keys(): limits['Out-of-plane'] = reduced_limits_database['Out-of-plane']['Interstorey'][support] * sum(thicknesses) # Return populated dictionary return limits
[docs]def viia_find_limits_NLTH(project: ViiaProject, shape: Shapes) -> Optional[Dict]: """ This function returns a dictionary with the checks to be performed on the passed shape as keys and the corresponding limit values as values for the passed shape for NLTH. Checks and limits are retrieved from the VIIA-LimitValues-NLTH-Database. Input: - project (obj): Project object containing collections and of fem objects and project variables. - shape (obj): Shape for which checks and limit values are retrieved. Output: - Returns a dictionary with the checks as keys and the corresponding limit values as values. """ # Get data from the database limits_database = _viia_get_database_limits(project=project) if not limits_database: warnings.warn( "WARNING: Yaml database with limit values for contours could not be opened. Limits for the NLTH compliance " f"check for {shape.name} is not performed.") return None # Determine generic material, first iteration of material name used for searching in database, and linearity initial_data = _viia_find_limits_material_data(material_name=shape.material.name) if not initial_data: warnings.warn( f"WARNING: Limit value contour functionality for material {shape.material.name} is not implemented. Please " f"report to the VIIA automation team.") return None else: generic_material, material, linearity = initial_data # Determine thicknesses (when applicable), and modify material name for searching in database thicknesses = [] # The name of the shape is used for identification instead of the name of the shape material because of how DURISOL, # LIN-HBV-PLANKEN, LIN-HBV-PLATEN and LIN-HSB naming works for index, thick_match in enumerate(re.finditer(r'-A?(\d+)(\.?)(\d*)(?=-)', shape.name)): # Thickness calculated according to 'LIN-HSB-0.012(a)-0.050(b)-0.075(c)-0.600(d)', t = (a) + (c) if material in ['LIN-HBV-PLANKEN', 'LIN-HBV-PLATEN', 'LIN-HSB']: if index in [0, 2]: thicknesses.append(float(thick_match.group(0).replace('-', ''))) else: thicknesses.append(float(thick_match.group(0).replace('-', '').replace('A', '')) / 1000) if generic_material in ['Concrete'] and not re.search(r'^((LIN-)?BETON)$', material): material += '-x' # Determine the strength class strength_class_match = re.search(r'-([CSB]\d+/?\d+)', shape.material.name) strength_class = 'Default' if strength_class_match: strength_class = strength_class_match.group(1) # Find interstorey and effective heights of shape and the number of storeys, if applicable interstorey_height = None eff_height = None storey_num = None if isinstance(shape, Wall): # Find layers of interest for interstorey and effective height calculations layers = [layer for layer in project.collections.layers if layer.name.startswith('N')] interstorey_height = _viia_find_interstorey_height(project=project, shape=shape, layers=layers) eff_height, storey_num = _viia_find_effective_height(project=project, shape=shape, layers=layers) # Determine shape type shape_type = shape.name.split('-')[1] # Default drift conditions and user warning. Determination of actual conditions may be implemented in the future ductility = 'Ductile' support = '1-way' warnings.warn( f"WARNING: Drift limits are calculated assuming a {ductility} failure mechanism and {support} support " f"conditions. Verify that this is correct for all shapes that are checked for drift in your project.") # Dictionary to return, with checks as keys and limit values as values limits = {} # List of lists that contain data for generic checks. The first sublist contains generic check names, the second # contains functions that correspond to these names, and third contains arguments that correspond to these functions generic_checks_data = [ ['Strength', 'Drift'], [_viia_find_strength_limits_nlth, _viia_find_drift_limits_nlth], [[shape, material, thicknesses, strength_class], [shape, interstorey_height, eff_height, storey_num, thicknesses, ductility, support]]] floor_layer_match = re.search(r'N(\d+)', shape.layer.name) if not isinstance(shape, (Wall, Floor, Roof)) or ( type(shape) is Floor and floor_layer_match and int(floor_layer_match.group(1)) < 1): for check_list in generic_checks_data: del check_list[1] # Populate the limits dictionary with data relevant to all geometries and the shape type for the generic checks for geometry in ['AllGeometries', shape_type]: for generic_check, func, args in zip(*generic_checks_data): try: reduced_database = deepcopy(limits_database[geometry][generic_check][generic_material]) except KeyError: continue # If applicable, populate the dictionary with data relevant to all materials encompassed by the # generic material and the linearity class. Linearity data supersedes generic material data. for general_keyword in ['All', linearity]: if general_keyword in reduced_database.keys(): limits.update( func(*args, reduced_database[general_keyword])) # If applicable, populate the dictionary with data relevant to the specific material. This data # supersedes linearity and generic material data. for specific_materials in reduced_database: if isinstance(specific_materials, tuple) and any(e == material for e in specific_materials): limits.update( func(*args, reduced_database[specific_materials])) break # Raise warning and return nothing if limits dictionary is empty if not limits: warnings.warn(f"Limit values not found for {shape}. Contour images will not be generated.") return # Return limits dictionary return limits
### =================================================================================================================== ### 4. End of script ### ===================================================================================================================