Source code for viiapackage.connections.helper_functions

### ===================================================================================================================
###   Helper functions
### ===================================================================================================================
# Copyright ©VIIA 2024

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

# General imports
from __future__ import annotations
import re
import yaml
import codecs
import math
from hashlib import sha1
from itertools import product
from collections import Counter
from typing import TYPE_CHECKING, List, Union, Set, FrozenSet, Dict, Tuple, Any, Optional

# References for functions and classes in the rhdhv_fem package
from rhdhv_fem.shapes import Shapes, Lines, Surfaces, Wall, Points
from rhdhv_fem.connections import Connections
from rhdhv_fem.shape_geometries import Node, Line
from rhdhv_fem.fem_math import fem_unit_vector, fem_dot_product_vector, fem_point_on_line, fem_compare_values, \
    fem_point_in_plane, fem_distance_coordinates, fem_average_points, fem_compare_coordinates, \
    fem_intersection_of_two_lines, fem_greater, fem_cross_product_vector
from rhdhv_fem.fem_tools import fem_find_object

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


### ===================================================================================================================
###   2. Functions
### ===================================================================================================================

[docs]def viia_unique_id_from_names(source: Shapes, target: Shapes) -> str: """ Takes list of strings and generates unique ID. Input: - list_of_names (list of str): List of strings. Output: - Returns unique string. """ list_repr = str(sorted([source.name, target.name])) return codecs.encode( codecs.decode(sha1( list_repr.encode('UTF-8')).hexdigest(), 'hex'), 'base64').decode().replace('/', '@').replace('=', '&')[:7]
def _viia_check_input(item: Union[list, dict], name: str) -> None: """ Function checks if a given item is a list or a dictionary. If the item is a dictionary, it checks if the keys are strings and the values a string, boolean, list or dictionary. If the item is a list, it checks if the items in the list are strings, lists or dictionaries. In case the items in the list or the values of the dictionary is a list or dictionary, this function is recursive. Input: - item (list, dict): Item to be checked in this function. - name (str): Name to be printed in the warnings. Output: - Raises an error if the input is not as required. """ if isinstance(item, dict): for key, value in item.items(): if not isinstance(key, str): raise ValueError(f"ERROR: Key [{str(key)}] in {name} is not a string.") if not isinstance(value, (str, bool)): if isinstance(value, (dict, list)): _viia_check_input(item=value, name=name) else: raise ValueError(f"ERROR: Value [{str(value)}] of key [{str(key)}] in {name} is not a string.") elif isinstance(item, list): for value in item: if not isinstance(value, str): if isinstance(value, (dict, list)): _viia_check_input(item=value, name=name) else: raise ValueError(f"ERROR: Value [{str(value)}] in {name} is not a string.") else: raise ValueError(f"ERROR: Input for {name} is not a list or a dict.")
[docs]def viia_get_existing_connections(project: ViiaProject) -> Set[FrozenSet[str]]: """ This function parses existing connection definitions so the pairs can be quickly checked. Any connection that does not contain a target (or source) connecting shape is omitted in the returned set. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. Output: - Returns set of frozensets of pairs of connecting shape names of the connection (source and target). """ pairs = set() for connection in project.collections.connections: if 'source_connecting_shape' in connection.connecting_shapes and \ 'target_connecting_shape' in connection.connecting_shapes: pairs.add(frozenset(( connection.connecting_shapes['source_connecting_shape'].name, # Source shape connection.connecting_shapes['target_connecting_shape'].name, # Target shape ))) return pairs
def _is_parallel(shape1: Lines, shape2: Lines): """ Determines if two shapes are parallel to each other. Currently only handles lines, if one of the shape does not belong to Lines the False is returned. Input: shape1 (Shape): shape object. shape2 (Shape): shape object. Output: bool: bool statement if shapes are parallel. """ if not isinstance(shape1, Lines): return False if not isinstance(shape2, Lines): return False unit_direction_1 = fem_unit_vector(_get_shape_direction(shape1)) unit_direction_2 = fem_unit_vector(_get_shape_direction(shape2)) product = min(max(abs(fem_dot_product_vector(unit_direction_1, unit_direction_2)), 0), 1) if not fem_greater(math.acos(product), math.pi / 180 * 5): return True def _connect_node(project: ViiaProject, node: Node, master_shape: Shapes, slave_shape: Shapes, dof: str): """ Connects two shapes with a common node by a specified hinge. Input: project (Project): Project object. node (Node): Node object that is common between two shapes. master_shape (Shape): Master or Source shape in the connection definition slave_shape (Shape): Slave or Target shape in the connection definition dof (str): Rotational degrees of freedom to be constrained by the hinge. e.g. "H_RRR", "H_RRF"; R stands for Rotation meaning that the degree of dreedom is released; F stands for Fixed meaning that degree of freedom is locked. """ dof_d = [] for symbol in dof[-3:]: if symbol == "R": dof_d.append(0) if symbol == "F": dof_d.append(1) # for trusses override dof as if 'STAAF' in master_shape.name: dof_d = [0, 0, 0] # Create unique name for the hinge hinge_name = \ f'{project.viia_get_highest_layer(layer1=master_shape.layer, layer2=slave_shape.layer, name=True)}-' \ f'HINGE-{dof[-3:]}-[{viia_unique_id_from_names(source=master_shape, target=slave_shape)}]' # 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 hinge_name + f"-{n + 1}" not in connection_names: hinge_name = hinge_name + f"-{n + 1}" break else: project.write_log(f"ERROR: Options for unique interface name are depleted for interface {hinge_name}") # Create hinge project.create_hinge( connecting_shapes={ 'source_connecting_shape': master_shape, 'source_shape_geometry': node, 'target_connecting_shape': slave_shape, 'target_shape_geometry': node}, degrees_of_freedom=dof_d, axes=[project.create_direction([1, 0, 0]), project.create_direction([0, 1, 0]), project.create_direction([0, 0, 1])], name=hinge_name)
[docs]def viia_load_connections_library(project: ViiaProject) -> \ Tuple[Any, Dict[FrozenSet[str], Dict[str, Union[int, Any]]]]: """ This function loads the yaml database with description of the interfaces in UPR. It processes the database for fast lookup. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. Output: - Returns dictionary of strings for fast connection lookup. """ # Read yaml with VIIA constants with open( project.viia_settings.project_specific_package_location / 'connections' / 'connections_library.yaml') as f: data = yaml.safe_load(f) # Process data for fast lookup lookup_pairs = {} for conn in data['Connections']: for n, definition in enumerate(data['Connections'][conn]): combinations = [] ignore_path = definition.get('ignore_path', False) for shape in ['shape_1', 'shape_2']: directions = _convert2list(definition[shape].get('direction', ['strong_axis', 'weak_axis'])) types = _convert2list(definition[shape].get('type', [])) materials = _convert2list(definition[shape].get('material', [])) orientations = _convert2list(definition[shape].get('orientation', ['vertical', 'sloped', 'horizontal'])) combinations.append(list(product(directions, types, materials, orientations))) for combination1 in combinations[0]: shape_1_string = _gen_identity_string( combination1[1], combination1[2], combination1[0], combination1[3]) for combination2 in combinations[1]: shape_2_string = _gen_identity_string( combination2[1], combination2[2], combination2[0], combination2[3]) key = frozenset((shape_1_string, shape_2_string,)) # Check for double definitions if key in lookup_pairs: if lookup_pairs[key]['connection'] != conn: project.write_log( f"WARNING: Some detail definitions are conflicting for pair {key} at pos {n} in " f"detail {conn} with {lookup_pairs[key]}.") else: # If all is fine register new definition lookup_pairs[key] = {'connection': conn, 'pos': n, 'ignore_path': ignore_path} return data, lookup_pairs
def _get_shape_direction(shape: Shapes): """ Returns "direction" of the shape specified. For 2d shapes element_x_axis is returned. For 1d shapes - a vector prom start to end nodes is returned. Input: shape (Shape): Shape object Output: List[float]: direction vector """ if isinstance(shape, Lines): return fem_unit_vector([x1 - x0 for x0, x1 in zip(shape.contour.node_start, shape.contour.node_end)]) elif isinstance(shape, Line): return fem_unit_vector([x1 - x0 for x0, x1 in zip(shape.node_start, shape.node_end)]) else: try: if isinstance(shape, Wall): return viia_get_wall_horizontal_direction(wall=shape).vector else: return shape.element_x_axis.vector except Exception: # Some elements do not have element_x_axis like point masses, these should return None return None def _get_identity_string_from_shape(shape: Shapes, direction: List[float]): """ Determines identity string from shape object. Retrieves all required information from the shape itself. Only direction has to be provided. Input: shape (Shape): shape object. direction (List[float]): direction vector of the shape within the connection Output: str: a string representing shape identity """ info = {} info['type_'] = _get_shape_type(shape.name) try: info['material'] = shape.material.name.replace('LIN-', "").split('-')[0] except AttributeError: print( f"ERROR: {shape.name} has no material or material has no name. Therefore, it cannot be used with automatic connections") info['material'] = "UNKNOWN" info['orientation'] = _get_oriention(shape) if isinstance(shape, Lines) or not direction: # If shape is line, no direction is applicable, therefore default state of strong_axis is assigned info['direction'] = 'strong_axis' else: # TODO: Handle sloped shapes, calculate horizontal projection vector = _get_shape_direction(shape) if vector: angle = fem_dot_product_vector(fem_unit_vector(vector), fem_unit_vector(direction)) # If connection angle between the shape strong axis and another shape is approx 40degrees if fem_greater(abs(angle), 0.75): info['direction'] = 'weak_axis' else: info['direction'] = 'strong_axis' else: info['direction'] = 'strong_axis' return _gen_identity_string(**info) def _wrong_load_path(shape1: 'Shape', shape2: 'Shape'): """ Determines if shapes specified form a transfer of vertical loads. When connections are assessed it is possible that a shape connecting to a bottom of supporting shape satisfies the conditions. These situations should be rejected as it is unlikely that the structure that is being supported is hanging from the supports. Input: shape1 (Shape): a shape object shape2 (Shape): a shape object Output: bool: bool statement if shapes form correct load path """ # Determine geometrical oriention of the shapes [vertical, horizontal, sloped] shape1_type = _get_oriention(shape1) shape2_type = _get_oriention(shape2) # If vertical/sloped and horizontal then vertical/sloped not above (on edge and above [if there are other vertical elements bellow]) if set((shape1_type, shape2_type,)) == set(("horizontal", "vertical",)) or \ set((shape1_type, shape2_type,)) == set(("horizontal", "sloped",)): connecting = shape1.get_connecting_lines(shape=shape2, include_openings=False) if not connecting: connecting = shape1.get_connecting_nodes(shape=shape2, include_openings=False) count = 0 for geometry in connecting: if _is_at_bottom(geometry, shape1) or _is_at_bottom(geometry, shape2): shapes = geometry.get_shapes() for shape in shapes: if _is_vertical(shape) and not _is_at_bottom(geometry, shape): count += 1 else: return False if count >= len(connecting): return True else: return False # If vertical and slopped then vertical not above if set((shape1_type, shape2_type,)) == set(("sloped", "vertical",)): connecting = shape1.get_connecting_lines(shape=shape2, include_openings=False) if not connecting: connecting = shape1.get_connecting_nodes(shape=shape2, include_openings=False) count = 0 for geometry in connecting: if shape1_type == "vertical" and _is_at_bottom(geometry, shape1) \ or shape2_type == "vertical" and _is_at_bottom(geometry, shape2): return True else: return False # Otherwise return False def _get_pairs_with_target(source: 'Shape', target: 'Shape'): """ Creates pairs for shapes with a target at the connection with the sources. Considers only line connections. Input: source (Shape): Source shape target (Shape): Target shape Output: set(frozenset): set of frozen set of pairs """ pairs = set() lines = source.get_connecting_lines(shape=target, include_openings=False) if lines: for line in lines: shapes = line.get_shapes() for shape in shapes: if shape != target: pair = frozenset((shape.name, target.name)) pairs.add(pair) return pairs def _convert2list(value: Union[list, float, str, tuple, bool]) -> Union[list,tuple]: """ Function that makes sure that value is a list or a tuple. If float, int or bool is passed then it is returned within a list Input: value (Union[list,float,str,tuple,bool]) - value to be converted to list Output: a list of values """ if isinstance(value, (list, tuple)): return value else: return [value, ] def _filter_lines(project, lines: List[Union['Lines', 'Line']], shapes: List['Shape']): """ Filters lines in the list to make sure they are in the shape and not on an opening. Args: lines (List[Union['Lines','Line']]): List of lines shape (List[Shape]): shapes to be check agains Output: List[Union['Lines','Line']]: List of filtered lines """ ok_lines = [] for line in lines: applicable = True for shape in shapes: contour = False opening = False if isinstance(shape, (Line, Lines)): continue try: check_value = _viia_line_in_surface_shape(project, line=line, surface_shape=shape) except AttributeError: # incase the shape is not supported allow a line. check_value = "unknown" if check_value == "on_contour" or not check_value: contour = True opening = shape.is_line_inside_opening(line) if opening and contour: applicable = False break if applicable: ok_lines.append(line) return ok_lines if len(ok_lines) != 0 else None def _get_shape_type(shape_name: str): """ Determine shape type from shape name. Regex match expects that the type is defined in following formats: [LEVEL]-[TYPE]-... or PAAL-... E.g.: F-WANDEN-LIN-MW-KLEI<1945-100-1; N0-STAAF-LIN-STAAL-D10-4; N2-BALKEN-STAAL-S235-SHS80x6-1 PAAL#64_TYPE_A Input: shape_name (str): name of a shape. Output: str: type of the shape extracted from the name """ if 'LIJN' in shape_name: return 'LIJNMASSA' match = re.search(r'^([FBN][0-9]*-(.*?))-|^PAAL', shape_name).groups() # Group 1 or Group 2 if match: return [x for x in match if x][-1] else: print( f"ERROR: {shape_name} is not according VIIA naming conventions. Therefore, it cannot be used in automatic connections") return 'UNKNOWN' def _gen_identity_string(type_: str, material: str, direction: str, orientation: str): """ Generates shape identity string based on shape type, material, direction and orientation Input: type_ (str): type of the shape like WANDEN, VLOEREN, etc. material (str): material of the shape like STAAL, BETON, MW, etc. direction (str): direction of the shape like strong_axis or weak_axis. orientation (str): orientation of the shape like vertical, horizontal or sloped. Output: str: a string representing shape identity """ return '_'.join([type_, material, direction, orientation]) def _get_oriention(shape: 'Shape'): """ A function that returns the orientation of a shape [horizontal, vertical or sloped] Input: shape (Shape): Shape object Output: str: a string stating the orientation of a shape. One of the options: [horizontal, vertical or sloped] """ if _is_flat(shape): return "horizontal" elif _is_vertical(shape): return "vertical" else: return "sloped" def _is_at_bottom(obj: Union['Line', 'Point'], shape: 'Shape'): """ Determine if obj [point or line] is at a bottom of a shape Input: obj (Union[Line,Point]): a point or a line of which possition needs to be determined shape (Shape): a reference shape Output: bool: bool statement if a point or a line is at the bottom of a shape. """ if isinstance(shape, Lines): return False if _is_flat(shape): return False obj_z = _avg([point[2] for point in obj.get_points()]) shape_z = min([x[2] for b in shape.contour for x in b]) if fem_compare_values(obj_z, shape_z): return True else: return False def _is_flat(obj: 'Shape'): """ Determines if shape is horizontal. Input: obj (Shape): Shape object Output: bool: bool statement if shape is horizontal. """ vertical = [0, 0, 1] try: normal = obj.normal_vector() except AttributeError: # Some shapes like point mass do not have a direction normal = [1, 0, 0] if not normal: normal = obj.element_z_axis.vector if isinstance(obj, (Line, Lines)): try: angle = fem_dot_product_vector( fem_unit_vector([y - x for x, y in zip(obj.contour.node_start, obj.contour.node_end)]), vertical) except AttributeError: angle = fem_dot_product_vector(fem_unit_vector([y - x for x, y in zip(obj.node_start, obj.node_end)]), vertical) return True if abs(angle) < 0.05 else False else: angle = fem_dot_product_vector(normal, vertical) return True if abs(angle) > 0.95 else False def _is_vertical(obj: 'Shape'): """ Determines if shape is vertical. Input: obj (Shape): Shape object Output: bool: bool statement if shape is vertical. """ vertical = [0, 0, 1] try: normal = obj.normal_vector() except AttributeError: # Some shapes like point mass do not have a direction normal = [1, 0, 0] if not normal: normal = obj.element_z_axis.vector if isinstance(obj, (Line, Lines)): try: angle = fem_dot_product_vector( fem_unit_vector([y - x for x, y in zip(obj.contour.node_start, obj.contour.node_end)]), vertical) except AttributeError: angle = fem_dot_product_vector(fem_unit_vector([y - x for x, y in zip(obj.node_start, obj.node_end)]), vertical) return True if abs(angle) > 0.95 else False else: angle = fem_dot_product_vector(normal, vertical) return True if abs(angle) < 0.05 else False def _avg(lst: List[float]): """ Calculate average value from a list of floats. Input: lst (List[float]): a list of floats. Output: float: an average of a list of floats """ return sum(lst) / len(lst) def _viia_line_in_surface_shape(project: ViiaProject, line: 'Line', surface_shape: Union[Surfaces, str]): """ This function checks if a line is within the surface of a surfaces shape. It checks if all the coordinates of the line are within or on the contour and it checks if all the coordinates are outside any openings. If it does, it is checked if all the coordinates of the line are on the contour. If so it will return "on_contour". WARNING: Note that if all the nodes of a line or on the surface this function returns True, but a line could cross an opening TODO add functionality which checks if a node between two node of the line are also within surface Input: - project (obj): object reference to the project. - line (reference to object of line): The line to check. - surface (reference to object): Input for surface as the reference to the object of the surface that needs to be checked. alternative (str): name of surface Output: - Returns a boolean for check if line is completly within surface or 'on_contour' if line is completly on the contour of the surface """ # TODO add the checking for openings # Check if target_surface has propper input if type(surface_shape) is str: surface_shape = fem_find_object(surface_shape, project.collections.surfaces) if surface_shape is None or surface_shape not in project.collections.surfaces: raise ValueError # Check if line is in plane of surface if not (fem_point_in_plane(point=line.get_startnode().coordinates, plane=[surface_shape.get_points()[0]], normal_vector=surface_shape.normal_vector()) or not fem_point_in_plane(point=line.get_endnode().coordinates, plane=[surface_shape.get_points()[0]], normal_vector=surface_shape.normal_vector())): return False # Check if all coordinates of the line are on the surface_shape check_for_external_lines = False points_on_shape = [] for node in line.get_nodes(): if not surface_shape.is_point_in_shape(node.coordinates): check_for_external_lines = True # at least one node is not on surface else: points_on_shape.append(node.coordinates) # TODO This code in if-statement runs slow, should be optimized # Check for lines which have points outside the surface but cross the surface, for these lines the segements which # are on the shape will be returned if check_for_external_lines: # check if the line has intersection with a contour line and if this intersection is on both lines. for surface_line in surface_shape.get_lines(): intersection = _viia_intersect_on_lines(project=project, line_1=line, line_2=surface_line) if intersection is not False: intersection_present = False for point_on_shape in points_on_shape: if fem_compare_coordinates(coordinate1=point_on_shape, coordinate2=intersection, precision=project.check_precision): intersection_present = True break if not intersection_present: points_on_shape.append(intersection) if len(points_on_shape) == 0 or len(points_on_shape) == 1: # no nodes and intersections are on shape return else: line_point_list = [] # find non zero dimension for sorting for dim in range(len(points_on_shape[0])): if abs(points_on_shape[0][dim] - points_on_shape[0][dim]) > 1/(10**project.check_precision): break def _sort_dimension(coordinate): # dummy function for sorting return coordinate[dim] points_on_shape.sort(key=_sort_dimension) # Check if a line segment is on the shape. This is done by checking if the mid point of the line is on the # shape and that is not on the contour (than the line is already part of contour polyline) or # on an opening edge (than the line is altreadt part of the opening polyline) or # in an opening (than the line goes from opening line to opening line) for i in range(1, len(points_on_shape)): line_points = [points_on_shape[i-1], points_on_shape[i]] if not round(fem_distance_coordinates(coordinate1=line_points[0], coordinate2=line_points[1]), project.check_precision) < 1/(10**project.check_precision): mid_point = fem_average_points(line_points) if (surface_shape.is_point_in_shape(mid_point) and not surface_shape.contour.is_point_on_contour(mid_point) and not surface_shape.surface_shape.is_point_on_opening_edge(point=mid_point) and not surface_shape.is_point_inside_opening(mid_point)): line_point_list.append(line_points) return line_point_list if not check_for_external_lines: # TODO now False is returned when one node is in an opening. but not the entire line has to be not on shape # check routine at which is used when check_for_external_lines is True how to check for line segments if surface_shape.is_point_inside_opening( fem_average_points([line.get_startnode().coordinates, line.get_endnode().coordinates])): return False # check if all points are on contour # first check if the surface has a contour if surface_shape.contour is not None: # Check if all nodes of line are on one line of the contour for contour_line in surface_shape.contour.get_lines(): for node in line.get_nodes(): all_points_on_same_contour_line = False if fem_point_on_line(point=node.coordinates, line_points=[contour_line.get_startnode().coordinates, contour_line.get_endnode().coordinates]): all_points_on_same_contour_line = True if not all_points_on_same_contour_line: break # a node of line is not on this contour_line check next contour line if all_points_on_same_contour_line: # when reaching here all nodes of line are on this contour_line # check if midpoint of line is on contour if surface_shape.contour.is_point_on_contour( fem_average_points([line.get_startnode().coordinates, line.get_endnode().coordinates])): return "on_contour" else: # if mid point is not on contour the whole line is probably not on contour return False # At least on of the point is not on the same contour line # check if midpoint is on surface if surface_shape.is_point_in_shape(fem_average_points(line.get_points())): return True else: # if midpoint is not on surface than the whole line is probably not on the surfaces return False def _viia_intersect_on_lines(project: ViiaProject, line_1: Line, line_2: Line): # TODO add docstring # TODO should be replaced by fem_intersection_of_two_line_segments intersection = fem_intersection_of_two_lines( coordinate1=line_1.get_startnode().coordinates, vector1=line_1.get_direction().vector, coordinate2=line_2.get_startnode().coordinates, vector2=line_2.get_direction().vector, precision=project.check_precision) if intersection not in ['NoIntersection', 'Parallel']: if fem_point_on_line( point=intersection, line_points=[line_1.get_startnode().coordinates, line_1.get_endnode().coordinates]): if fem_point_on_line( point=intersection, line_points=[line_2.get_startnode().coordinates, line_2.get_endnode().coordinates]): return intersection return False
[docs]def viia_get_input_for_hinges( project: ViiaProject, source: Union[str, Shapes], target: Union[str, Shapes]) -> Tuple[Shapes, Shapes]: """ Function to get the source and target shapes for generation of a hinge. 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 hinge. Alternative the name can be provided as string. - target (obj): Object reference of the target shape for the hinge. 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. """ if isinstance(source, str): source = fem_find_object(source, project.collections.shapes) if source is None: raise ValueError(f"ERROR: Source shape for creation of hinge could not be found in project.") if not isinstance(source, (Surfaces, Lines, Points)): raise ValueError(f"ERROR: Source shape for creation of hinge 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 creation of hinge could not be found in project.") if not isinstance(target, (Surfaces, Lines, Points)): raise ValueError(f"ERROR: Target shape for creation of hinge 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.") return source, target
def _get_connecting_shape_geometries_for_hinges( project: ViiaProject, source: Union[Shapes], target: Union[Shapes], detail=Union[str]) \ -> List[Union[Node, Line]]: """ Function to get the connecting shape geometries for generation of a hing, depending on if it is a point or a line hinge. It will raise an error if the source and target do not have shared nodes/lines. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - source (obj): Object reference of the source shape for the hinge - target (obj): Object reference of the target shape for the hinge - detail (str): 'HINGE-P' or 'HINGE-L' Output: - Returns the connecting shape geometries based on the hinge geometry (point or line-hinge). """ if detail == 'HINGE-P': # Get the shared nodes of the shapes and check if there are any shared nodes. If there are no shared # nodes the shapes are connected again. If there are, after this action, still no connecting nodes this # function will be aborted. connecting_node = source.get_connecting_nodes(shape=target, include_openings=False) if len(connecting_node) == 0: project.write_log( f"WARNING: Shape {source.name} and shape {target.name} have no connecting nodes. Now it's tried to " f"reconnect the shapes.") # Try to reconnect project.viia_connect_shape(source) project.viia_connect_shape(target) connecting_node = source.get_connecting_nodes(shape=target, include_openings=False) # Second check if shared nodes are found if len(connecting_node) == 0: raise ValueError( f"ERROR: Shape {source.name} and shape {target.name} have no connecting nodes. No point-hinge can be " f"made between these shapes.") return connecting_node elif detail == 'HINGE-L': # Get the shared lines of the shapes and check if there are any shared lines. If there are no shared # lines the shapes are connected again. If there are, after this action, still no connecting lines this # function will be aborted. connecting_lines = source.get_connecting_lines(shape=target, include_openings=False) if len(connecting_lines) == 0: project.write_log( f"WARNING: Shape {source.name} and shape {target.name} have no connecting lines. Now it's tried to " f"reconnect the shapes.") # Try to reconnect project.viia_connect_shape(source) project.viia_connect_shape(target) connecting_lines = source.get_connecting_lines(shape=target, include_openings=False) # Second check if shared nodes are found if len(connecting_lines) == 0: raise ValueError( f"ERROR: Shape {source.name} and shape {target.name} have no connecting nodes. No point-hinge can be " f"made between these shapes.") return connecting_lines else: raise ValueError( f"ERROR: Only point and line hinges can be applied. Please specify detail as 'HINGE-P' or 'HINGE-L.") def _generate_name_for_hinges(project: ViiaProject, source: Shapes, target: Shapes, detail: Union[str], dof: Optional[List[float]] = None) -> str: """ Function to generate a name for the hinge. 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. - source (obj): Object reference of the source shape for the hinge - target (obj): Object reference of the target shape for the hinge Output: - Returns name as a string. """ # Get the uppermost level for the interface name layer = project.viia_get_highest_layer(layer1=source.layer, layer2=target.layer) dof_str = "" for dir in dof: if dir == 0: dof_str += 'R' elif dir == 1: dof_str += 'F' # Generate the name for the connection and add unique ID identifier based on combination of source and target name = f'{layer.name}-HINGE-{dof_str}-[{viia_unique_id_from_names(source, target)}]' # Get list of existing hinges 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 viia_create_non_hinge_connection( project: ViiaProject, source: Union[Surfaces, Lines, str], target: Union[Surfaces, Lines, str], detail: str, is_linear=False, thickness_interface: Optional[float] = None, area_interface: Optional[float] = None, switch: bool = False, flip: bool = False, local_x_axis: Optional[list] = None, local_y_axis: Optional[list] = None) -> List[Connections]: """ Function to connect two shapes with a certain detail as described in Basis of Design (UPR). .. note:: Before using this function make sure the two shapes are connected. For this the project.viia_connect_shape function can be used. .. warning:: Check your result in the model, as there are a lot of exceptions known to cause an error when applying the function, when meshing or running the model in DIANA. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - source (obj): Object reference of source shape. This might be a surface or line-shape and depends on the requested detail. Alternative (str): The name of the source shape can be provided. - target (obj): Object reference of target shape. This might be a surface or line-shape and depends on the requested detail. Alternative (str): The name of the target shape can be provided. - detail (str): Detail number as described in the Basis of Design (UPR). For example 'D2.01'. For hinges, it should be specified as 'HINGE-P' for point hinges and as 'HINGE-L' for line hinges. - is_linear (bool): Select to apply linear material properties, default value is False. - thickness_interface (float): Option to manually set interface thickness for line interfaces, in [m]. **Required for: D3.03.** - area_interface (float): Option to manually set interface area for point interfaces, in [m2]. - switch (bool): Option to switch the source and target shapes. If True, shapes are switched. Unlike the flip, the switch is performed in the geometry model. Default value is false. - flip (bool): Option to flip the local z-axis of the interface. If neither local_x_axis nor local_y_axis are defined, then the flip is done *after* the local axes have been corrected after making the model. The flip is only performed after running viia_create_model(). Default value is False. - local_x_axis (list): Option for point interfaces to manually define the local x-axis of the interface. - local_y_axis (list): Option for point and line interfaces to manually define the local y-axis of the interface. **Required for: D3.03.**. Output: - The connection is created in PY-memory. For example: >>> viia_create_connection(Wall1, Floor1, 'D2.01') """ from viiapackage.connections import Detail from viiapackage.connections.unite_detail import UniteDetail from viiapackage.connections.no_connection_detail import NoConnectionDetail # Collect the detail information detail = Detail.get_detail(project=project, detail_nr=detail) # Validate the source and target shape (and convert to object if provided by name) source, target = detail.validate_input(project=project, source=source, target=target) # Start logging creation process project.write_log(f"Start creating interface {detail.detail_nr} on {source.name} and {target.name}.") # Get the connecting shape geometries (nodes for point connections and lines for line connections) connecting_shape_geometries = detail.get_connecting_shape_geometries(project=project, source=source, target=target) if not connecting_shape_geometries: project.write_log( f"WARNING: No connecting shape geometries found between {source} and {target}. Detail {detail} cannot be " f"applied. Function will be aborted") return [] # Create the connecting shape dictionaries, geometries and axes for point and line interfaces connecting_shapes = [] if detail.geometry_type == 'point': # Point connection for node in connecting_shape_geometries: if not switch: connecting_shapes.append({ 'source_connecting_shape': source, 'target_connecting_shape': target, 'source_shape_geometry': node, 'target_shape_geometry': node}) else: connecting_shapes.append({ 'source_connecting_shape': target, 'target_connecting_shape': source, 'source_shape_geometry': node, 'target_shape_geometry': node}) # If area_interface is not defined by user, then the area is calculated based on detail settings if not area_interface and hasattr(detail, 'get_area_interface'): area_interface = detail.get_area_interface(source=source, target=target) if hasattr(detail, 'create_geometry'): geometry = detail.create_geometry( project=project, name=f'PUNT-IF-{area_interface:.2f}', area=area_interface) else: geometry = None if hasattr(detail, 'get_local_axes'): if not local_x_axis or not local_y_axis: if local_x_axis or local_y_axis: project.write_log( f"WARNING: Inconsistent input for local interface axes, attempting to find default axes for" f" detail {detail.detail_nr}.") axes = detail.get_local_axes( project=project, source=source, target=target, connecting_node=connecting_shape_geometries[0]) else: axes = detail.get_local_axes( project=project, source=source, target=target, local_x_axis=local_x_axis, local_y_axis=local_y_axis) else: axes = None else: # Line connections for line in connecting_shape_geometries: if not switch: connecting_shapes.append({ 'source_connecting_shape': source, 'target_connecting_shape': target, 'source_shape_geometry': line, 'target_shape_geometry': line, 'source_anchor_node': line.get_startnode(), 'target_anchor_node': line.get_startnode()}) else: connecting_shapes.append({ 'source_connecting_shape': target, 'target_connecting_shape': source, 'source_shape_geometry': line, 'target_shape_geometry': line, 'source_anchor_node': line.get_startnode(), 'target_anchor_node': line.get_startnode()}) # If thickness_interface is not defined by user, then the thickness is calculated based on detail settings if not thickness_interface and hasattr(detail, 'get_thickness_interface'): thickness_interface = detail.get_thickness_interface(source=source, target=target) if hasattr(detail, 'create_geometry'): geometry = detail.create_geometry( project=project, name=f'LIJN-IF-{thickness_interface * 1000:.0f}', thickness=thickness_interface) else: geometry = None if hasattr(detail, 'get_local_axes'): if local_x_axis is not None: project.write_log( f"WARNING: Inconsistent input for line interface {detail.detail_nr}, the local x-axis can't be " f"set. Input is ignored.") if local_y_axis: axes = detail.get_local_axes( project=project, source=source, target=target, local_y_axis=local_y_axis) else: axes = detail.get_local_axes( project=project, source=source, target=target, connecting_line=connecting_shape_geometries[0]) else: axes = None # Check for type of connection if isinstance(detail, NoConnectionDetail): connections = detail.create(project=project, connecting_shapes=connecting_shapes) elif isinstance(detail, UniteDetail): connections = detail.create(project=project, connecting_shapes=connecting_shapes) else: # Create the properties for the interface (material and data) material = detail.create_material(project=project, is_linear=is_linear) data = detail.create_data(project=project, is_linear=is_linear) # Create the interfaces for the connection between the two provided structural shapes connections = [] for connection in connecting_shapes: # Creation of the connection object connections.append(project.create_interface( name=detail.generate_name( project=project, source=source, target=target, area=area_interface, thickness=thickness_interface), material=material, geometry=geometry, data=data, connecting_shapes=connection, axes=axes)) if project.rhdhvDIANA.diana_version and \ float(project.rhdhvDIANA.diana_version.replace('diana ', '')) < 10.5: # The axes properties of the interface will be changed after the mesh by checking the local z axis. connections[-1].flip = flip else: if flip: # The axes properties of the interface are changed directly. No need to do it after meshing. connections[-1].flip_around_x_axis() # Check for nodes that should be disconnected within the line-disconnect. This is often for shapes perpendicular to # the shape that is being disconnected and only share one node. if isinstance(detail, NoConnectionDetail) and detail.geometry_type == 'line': nodes = Counter(sum([line.get_nodes() for line in connecting_shape_geometries], [])) duplicate_nodes = [k for k, v in nodes.items() if v >= 2] for duplicate_node in duplicate_nodes: sources = duplicate_node.get_shapes() connecting_shapes = [{ 'source_connecting_shape': source_shape, 'target_connecting_shape': target, 'source_shape_geometry': duplicate_node, 'target_shape_geometry': duplicate_node} for source_shape in sources if not list(set(connecting_shape_geometries).intersection(source_shape.get_lines()))] detail_no_connection = Detail.get_detail(project=project, detail_nr='NOCON-P') conns = detail_no_connection.create(project=project, connecting_shapes=connecting_shapes) for con in conns: connections.append(con) # General logging of interface creation if len(connections) == 1: project.write_log( f"{connections[0].name} connection has been successfully added to shape {source.name} and " f"{target.name}.") elif len(connections) > 1: project.write_log( f"{len(connections)} connections have been successfully added to shape {source.name} and {target.name}:" f"{' , '.join([con.name for con in connections])}") return connections
[docs]def viia_create_hinge_connection( project: ViiaProject, source: Union[Surfaces, Lines, str], target: Union[Surfaces, Lines, str], detail: str, local_x_axis: Optional[list] = None, local_y_axis: Optional[list] = None, degrees_of_freedom: Optional[List[int]] = None): """ Function to connect two shapes with a hinged connection. .. note:: Before using this function make sure the two shapes are connected. For this the project.viia_connect_shape function can be used. Input: - project (obj): VIIA project object containing collections of fem objects and project variables. - source (obj): Object reference of source shape. This might be a surface or line-shape and depends on the requested detail. Alternative (str): The name of the source shape can be provided. - target (obj): Object reference of target shape. This might be a surface or line-shape and depends on the requested detail. Alternative (str): The name of the target shape can be provided. - detail (str): Detail number as described in the Basis of Design (UPR). For example 'D2.01'. For hinges, it should be specified as 'HINGE-P' for point hinges and as 'HINGE-L' for line hinges. - local_x_axis (list): Option to specify the local x-axis for hinges. If for hinges, please also specify the next argument local_y_axis. - local_y_axis (list): Option to specify the local y-axis of a hinge. If for hinges, please also specify the argument local_x_axis. - degrees_of_freedom (list with 3 integers): List with the rotational degrees of freedom to be tied when creating a hinge. The list contains the rotational degrees of freedom or rotation about the local x-, y- and z-axes. Output: - The connection is created in PY-memory. For example: >>> viia_create_connection(Wall1, Floor1, 'HINGE-L') """ # Validate the source and target shape (and convert to object if provided by name) source, target = viia_get_input_for_hinges(project=project, source=source, target=target) # Get the connecting shape geometries (nodes for point hinges and lines for line hinges) connecting_shape_geometries = _get_connecting_shape_geometries_for_hinges(project=project, source=source, target=target, detail=detail) # Create the connecting shape dictionaries, geometries and axes for point and line interfaces connecting_shapes = [] for shape_geometry in connecting_shape_geometries: connecting_shapes.append({ 'source_connecting_shape': source, 'target_connecting_shape': target, 'source_shape_geometry': shape_geometry, 'target_shape_geometry': shape_geometry}) # Create the interfaces for the connection between the two provided structural shapes connections = [] for connection in connecting_shapes: # Create axes direction based on provided local_x_axis and local_y_axis axes = None if local_x_axis and local_y_axis: dir_x = project.create_direction(vector=local_x_axis) dir_y = project.create_direction(vector=local_y_axis) dir_z = project.create_direction(vector=fem_cross_product_vector(a=local_x_axis, b=local_y_axis)) axes = [dir_x, dir_y, dir_z] elif local_x_axis and not local_y_axis: project.write_log( f"WARNING: For creation of hinge, you need to also provide local y axis along with local x axis. " f"Default axes directions are used") elif not local_x_axis and local_y_axis: project.write_log( f"WARNING: For creation of hinge, you need to also provide local x axis along with local y axis. " f"Default axes directions are used") # Specify default for degrees of freedom if not specified. # By default, all rotational degrees of freedom are released if not degrees_of_freedom: degrees_of_freedom = [0, 0, 0] # Creation of the connection object connections.append(project.create_hinge( name=_generate_name_for_hinges( project=project, source=source, target=target, detail=detail, dof=degrees_of_freedom), connecting_shapes=connection, axes=axes, degrees_of_freedom=degrees_of_freedom)) return connections
### =================================================================================================================== ### 3. End of script ### ===================================================================================================================