Source code for viiapackage.connections.details

### ===================================================================================================================
###   Detail class
### ===================================================================================================================
# Copyright ©VIIA 2024

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

# General imports
import yaml
import abc
import inspect
from typing import Optional, Union, Tuple, Type, List

# References for functions and classes in the rhdhv_fem package
from rhdhv_fem.shapes import Shapes, Surfaces, Lines, Wall, Floor, Beam, Column, Fstrip, Roof, Points
from rhdhv_fem.fem_tools import fem_find_object, fem_remove_duplicates_from_list
from rhdhv_fem.shape_geometries import Node, Line

# References for functions and classes in the viiaPackage
from viiapackage.connections.helper_functions import viia_unique_id_from_names
from viiapackage.viiaSettings import ViiaSettings


### ===================================================================================================================
###   2. Class Detail
### ===================================================================================================================

[docs]class Detail: """This is the class with information from the UPR regarding the detail numbers for connections.""" shapes = {'wall': Wall, 'floor': Floor, 'beam': Beam, 'column': Column, 'fstrip': Fstrip, 'roof': Roof}
[docs] def __init__( self, detail_nr: str, geometry_type: str, source_shape_type: Optional[Type[Shapes]] = None, target_shape_type: Optional[Type[Shapes]] = None): """ Input: - detail_nr (str): Name of the requested detail, see the Basis of Design (UPR). - geometry_type (str): The type of geometry for the no-connection detail. Select from 'point' or 'line'. - source_shape_type (cls): Class for the source shape-type. The provided source shape will be checked if it is an instance of this class. Available are classes that inherit from Shapes. Optional input, if not provided the type of class will not be checked. - target_shape_type (cls): Class for the target shape-type. The provided target shape will be checked if it is an instance of this class. Available are classes that inherit from Shapes. Optional input, if not provided the type of class will not be checked. """ # See property methods self.detail_nr = detail_nr self.geometry_type = geometry_type self.source_shape_type = source_shape_type self.target_shape_type = target_shape_type
@property def detail_nr(self): return self.__detail_nr @detail_nr.setter def detail_nr(self, new_detail_nr: str): if not isinstance(new_detail_nr, str): raise ValueError("ERROR: The detail-nr should be a string.") self.__detail_nr = new_detail_nr.upper() @property def geometry_type(self): return self.__geometry_type @geometry_type.setter def geometry_type(self, new_geometry_type: str): if not isinstance(new_geometry_type, str) or new_geometry_type.lower() not in ['point', 'line']: raise ValueError("ERROR: The detail should have a geometry-type of 'point' or 'line'.") self.__geometry_type = new_geometry_type.lower() @property def source_shape_type(self): return self.__source_shape_type @source_shape_type.setter def source_shape_type(self, new_source_shape_type: Optional[Type[Shapes]] = None): if new_source_shape_type is not None: if not inspect.isclass(new_source_shape_type): if isinstance(new_source_shape_type, str): new_source_shape_type = Detail.shapes[new_source_shape_type.lower()] else: raise ValueError( f"ERROR: The class for the source shape '{new_source_shape_type}' of detail '{self.detail_nr}' " f"could not be retrieved.") if not issubclass(new_source_shape_type, Shapes): raise ValueError( f"ERROR: The class for the source shape '{new_source_shape_type.__name__}' of detail " f"'{self.detail_nr}' is not an instance of the Shapes class.") self.__source_shape_type = new_source_shape_type @property def target_shape_type(self): return self.__target_shape_type @target_shape_type.setter def target_shape_type(self, new_target_shape_type: Optional[Type[Shapes]] = None): if new_target_shape_type is not None: if not inspect.isclass(new_target_shape_type): if isinstance(new_target_shape_type, str): new_target_shape_type = Detail.shapes[new_target_shape_type.lower()] else: raise ValueError( f"ERROR: The class for the target shape '{new_target_shape_type}' of detail '{self.detail_nr}' " f"could not be retrieved.") if not issubclass(new_target_shape_type, Shapes): raise ValueError( f"ERROR: The class for the target shape '{new_target_shape_type.__name__}' of detail " f"'{self.detail_nr}' is not an instance of the Shapes class.") self.__target_shape_type = new_target_shape_type
[docs] @abc.abstractmethod def name(self): """ Abstract method of 'Detail' to return the VIIA name for the connection."""
[docs] @staticmethod def get_detail(project: 'ViiaProject', detail_nr: str): """ This method of 'Detail' class collects the detail info from the viia-constants yaml and returns an instance of Detail class with this info. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - detail_nr (str): Name of the requested detail, see the Basis of Design (UPR). Output: - Returns Detail object if detail is recognised. """ # Imports from viiapackage.connections.no_connection_detail import NoConnectionDetail from viiapackage.connections.unite_detail import UniteDetail from viiapackage.connections.interface_detail import InterfaceDetail # Read yaml with VIIA constants with open(project.viia_settings.project_specific_package_location / 'viia_constants.yaml') as f: details = yaml.load(f, Loader=yaml.FullLoader)['Details'] # Check if detail is known if detail_nr.upper() not in details: raise KeyError(f"ERROR: The detail number {detail_nr} is unknown, please check your input.") # Create and return the Detail instance if 'no_interface' in details[detail_nr.upper()]: if 'source_shape_type' in details[detail_nr.upper()] and 'target_shape_type' in details[detail_nr.upper()]: return NoConnectionDetail( detail_nr=detail_nr, geometry_type=details[detail_nr.upper()]['geometry_type'], source_shape_type=details[detail_nr.upper()]['source_shape_type'], target_shape_type=details[detail_nr.upper()]['target_shape_type']) else: return NoConnectionDetail( detail_nr=detail_nr, geometry_type=details[detail_nr.upper()]['geometry_type']) elif 'unite' in details[detail_nr.upper()]: return UniteDetail(detail_nr=detail_nr, geometry_type=details[detail_nr.upper()]['geometry_type']) else: return InterfaceDetail(detail_nr=detail_nr, **details[detail_nr.upper()])
[docs] def validate_input( self, project: 'ViiaProject', source: Union[str, Shapes], target: Union[str, Shapes]) \ -> Tuple[Shapes, Shapes]: """ Method of 'Detail' to validate if the provided source and target shape match the requested classes for the specified detail. It will also convert shapes provided as string to the object reference based on the name. It will raise an error if any input is not valid. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - source (obj): Object reference of the source shape for the detail. Alternative the name can be provided as string. - target (obj): Object reference of the target shape for the detail. Alternative the name can be provided as string. Output: - Returns a tuple with the source and target shape. These are object references for the instances in Shapes class, which are valid for this detail. """ if isinstance(source, str): source = fem_find_object(source, project.collections.shapes) if source is None: raise ValueError(f"ERROR: Source shape for connection {self.detail_nr} could not be found in project.") if source.project is not project: raise ValueError( f"ERROR: Provided source shape for connection {self.detail_nr} is not part of the project.") if not isinstance(source, (Surfaces, Lines, Points)): raise ValueError( f"ERROR: Source shape for connection {self.detail_nr} is not a surface, line or point-shape.") if isinstance(target, str): target = fem_find_object(target, project.collections.shapes) if target is None: raise ValueError(f"ERROR: Target shape for connection {self.detail_nr} could not be found in project.") if target.project is not project: raise ValueError( f"ERROR: Provided target shape for connection {self.detail_nr}is not part of the project.") if not isinstance(target, (Surfaces, Lines, Points)): raise ValueError( f"ERROR: Target shape for connection {self.detail_nr} is not a surface, line or point-shape.") # Check if both shapes have a layer (this is required) if source.layer is None: raise ValueError( f"ERROR: Layer not set for shape {source.name}. Please set layers before applying connections.") if target.layer is None: raise ValueError( f"ERROR: Layer not set for shape {target.name}. Please set layers before applying connections.") # Check if the detail can be applied on the provided source and target shape if self.source_shape_type and not isinstance(source, self.source_shape_type): raise ValueError( f"ERROR: The detail {self.detail_nr} requires a source shape from " f"{self.source_shape_type.__name__} class, but {source.__class__.__name__} was provided.") if self.target_shape_type and not isinstance(target, self.target_shape_type): raise ValueError( f"ERROR: The detail {self.detail_nr} requires a target shape from " f"{self.target_shape_type.__name__} class, but {target.__class__.__name__} was provided.") if hasattr(self, 'validate_specific_input'): self.validate_specific_input(source=source, target=target) return source, target
[docs] def get_connecting_shape_geometries( self, project: 'ViiaProject', source: Shapes, target: Shapes, nodes: Optional[Union[Node, List[Node]]] = None, lines: Optional[Union[Line, List[Line]]] = None) \ -> List[Union[Node, Line]]: """ Method of 'Detail' class to find the connecting shape geometries. Depending on the detail settings a point or line connection needs to be created. This method will try to find the requested connecting shape geometries. It can be overruled by providing specific shape geometries. These are checked versus the requirements of the detail. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - source (obj): Object reference of the source shape for the detail. - target (obj): Object reference of the target shape for the detail. - nodes (list of nodes): List of object references for nodes (shape-geometries). This list is used to overrule the auto-function. This input will be ignored for line connections. Alternative single node. - lines (list of lines): List of object references for lines (shape-geometries). This list is used to overrule the auto-function. This input will be ignored for point connections. Alternative single line. Output: - Returns the connecting shape geometries based on the geometry-type (point or line-connection) of the requested detail. This list is auto generated, but can be overruled by user. """ def get_invalid_opening_nodes(shape, shape_line_opening_dict, shape_opening_nodes): """" Gets nodes that are completely surrounded by openings (non-structural nodes) Input: - shape (obj): Shape - shape_line_opening_dict (dict): Dict with a mapping for the shape of the lines and the openings - shape_opening_nodes (list of obj): All the nodes of the openings of the shape Output: - List of nodes that are completely surrounded by openings """ shape_contour_lines = shape.contour.get_lines() valid_shape_opening_lines = [ line for line, openings in shape_line_opening_dict.items() if len(openings) < 1 and line not in shape_contour_lines] valid_shape_opening_nodes = [] shape_lines = [line for line in shape.get_lines() if line not in shape_line_opening_dict] [valid_shape_opening_nodes.extend(line.get_nodes()) for line in valid_shape_opening_lines + shape_lines] valid_shape_opening_nodes = fem_remove_duplicates_from_list(valid_shape_opening_nodes) invalid_shape_opening_nodes = [ node for node in shape_opening_nodes if node not in valid_shape_opening_nodes] return invalid_shape_opening_nodes if self.geometry_type == 'point' and nodes: if not all([isinstance(node, Node) for node in nodes]): raise TypeError(f"ERROR: The input for nodes should be a list of Node instances. Provided was {nodes}.") return sorted(sorted(nodes, key=lambda node: node.coordinates[2]), key=lambda node: node.coordinates[1]) if self.geometry_type == 'line' and lines: if isinstance(lines, Line): lines = [lines] elif not all([isinstance(line, Node) for line in lines]): raise TypeError(f"ERROR: The input for lines should be a list of Line instances. Provided was {lines}.") return sorted(sorted( lines, key=lambda line: line.node_start.coordinates[2]), key=lambda line: line.node_start.coordinates[1]) lines = source.get_connecting_lines(target) source_line_opening_dict, target_line_opening_dict = {}, {} opening_lines, contour_lines, source_opening_nodes, target_opening_nodes = [], [], [], [] if lines: all_openings = [] for _list in \ [ [source, source_line_opening_dict, source_opening_nodes], [target, target_line_opening_dict, target_opening_nodes] ]: openings = getattr(_list[0], 'openings', []) if not openings: continue all_openings.extend(openings) for opening in openings: _list[2].extend(opening.get_nodes()) for line in opening.get_lines(): if line not in _list[1]: _list[1][line] = [] _list[1][line].append(line) opening_lines = fem_remove_duplicates_from_list( [line for opening in all_openings for line in opening.lines]) contour_lines = fem_remove_duplicates_from_list(source.contour.get_lines() + target.contour.get_lines()) source_opening_nodes = fem_remove_duplicates_from_list(source_opening_nodes) target_opening_nodes = fem_remove_duplicates_from_list(target_opening_nodes) lines_to_exclude = [] lines_to_exclude.extend([line for line, openings in source_line_opening_dict.items() if len(openings) > 1]) lines_to_exclude.extend([line for line, openings in target_line_opening_dict.items() if len(openings) > 1]) lines_to_exclude.extend([line for line in contour_lines if line in opening_lines]) lines_to_exclude = fem_remove_duplicates_from_list(lines_to_exclude) lines = [line for line in lines if line not in lines_to_exclude] if self.geometry_type == 'point': nodes_source = source.get_nodes() nodes_target = target.get_nodes() nodes = [node for node in nodes_source if node in nodes_target] invalid_source_opening_nodes = get_invalid_opening_nodes( shape=source, shape_line_opening_dict=source_line_opening_dict, shape_opening_nodes=source_opening_nodes) invalid_target_opening_nodes = get_invalid_opening_nodes( shape=target, shape_line_opening_dict=target_line_opening_dict, shape_opening_nodes=target_opening_nodes) _line_nodes = [] if lines: [_line_nodes.extend(line.get_nodes()) for line in lines] _line_nodes = fem_remove_duplicates_from_list(_line_nodes) nodes = [ node for node in nodes if node not in invalid_source_opening_nodes+invalid_target_opening_nodes+_line_nodes] return nodes elif self.geometry_type == 'line': return lines return []
[docs] def generate_name( self, project: 'ViiaProject', source: Shapes, target: Shapes, thickness: Optional[float] = None, area: Optional[float] = None) -> str: """ Method of 'Detail' class to generate a name for the connection. It collects the properties requires to create a unique name, complying to VIIA standard. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - detail_nr (str): Name of the requested detail, see the Basis of Design (UPR). Output: - Returns Detail object if detail is recognised. """ # Get the uppermost level for the interface name layer = project.viia_get_highest_layer(layer1=source.layer, layer2=target.layer) # Generate the name for the connection if self.geometry_type == 'point' and area: name = f'{layer.name}-{self.name}-{self.detail_nr}-{area:.2f}' elif self.geometry_type == 'line' and thickness: name = f'{layer.name}-{self.name}-{self.detail_nr}-{thickness* 1000:.0f}' else: name = f'{layer.name}-{self.name}-{self.detail_nr}' # Add unique ID identifier based on combination of source and target name += f'-[{viia_unique_id_from_names(source, target)}]' # Get list of existing connection names and check if the name is defined connection_names = set([conn.name for conn in project.collections.connections]) for n in range(ViiaSettings.NAME_COUNTER_LIMIT): if f'{name}-{n + 1}' not in connection_names: name += f'-{n + 1}' return name else: raise ValueError(f"ERROR: Options for unique connection name are depleted for connection-detail {name}.")
[docs] def get_master_slave_relationship(self, project, shape_1: Shapes, shape_2: Shapes): """ This method of 'Detail' determines correct allocation of slave and master based on the detail library. Two shapes can be provided and if possible the master and the slave is determined. Input: project (Project): Project object. shape_1 (Shape): Shape object. shape_2 (Shape): Shape object. Output: (Shape): master shape (Shape): slave shape List[Node,Line]: if connection is applicable a list of connecting shapes is returned. """ if self.source_shape_type and self.target_shape_type: if isinstance(shape_1, self.source_shape_type) and isinstance(shape_2, self.target_shape_type): master = shape_1 slave = shape_2 elif isinstance(shape_1, self.target_shape_type) and isinstance(shape_2, self.source_shape_type): slave = shape_1 master = shape_2 else: project.write_log( f"ERROR: Automatic connection is defined between shapes {shape_1.name} and {shape_2.name} with " f"detail {self.detail_nr}, but it cannot be connected because the types of these shapes are not " f"allowed for this detail.") return shape_1, shape_2, False else: master = shape_1 slave = shape_2 applicable = self.get_connecting_shape_geometries(project=project, source=master, target=slave) return master, slave, applicable
### =================================================================================================================== ### 3. End of script ### ===================================================================================================================