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