### ===================================================================================================================
### 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
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:
if isinstance(detail, (UniteDetail, NoConnectionDetail)):
connecting_shapes.append({
'source_connecting_shape': source,
'target_connecting_shape': target,
'source_shape_geometry': line,
'target_shape_geometry': line})
else:
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:
if isinstance(detail, (UniteDetail, NoConnectionDetail)):
connecting_shapes.append({
'source_connecting_shape': target,
'target_connecting_shape': source,
'source_shape_geometry': line,
'target_shape_geometry': line})
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
### ===================================================================================================================