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