Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mesh Modifications & Load Combinations #206

Open
wants to merge 6 commits into
base: DKMQ
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions PyNite/Analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,41 @@ def _prepare_model(model):
# Assign an internal ID to all nodes and elements in the model. This number is different from the name used by the user to identify nodes and elements.
_renumber(model)

def _identify_combos(model, combo_tags=None):
"""Returns a list of load combinations that are to be run based on tags given by the user.
def _identify_combos(model, combo_tags:list=None,load_combos:list=None):
"""Returns a list of load combinations that are to be run based on tags or names given by the user. If neither combo tags or names is given all load combinations will be returned.

:param model: The model being analyzed.
:type model: FEModel3D
:param combo_tags: A list of tags used for the load combinations to be evaluated. Defaults to `None` in which case all load combinations will be added to the list of load combinations to be run.
:param combo_tags: A list of tags used for the load combinations to be evaluated. Defaults to `None` in which case all load combinations will be added to the list of load combinations to be run, unless `load_combos` is not `None`.
:type combo_tags: list, optional
:param load_combos: A list of load combination names to be evaluated. Defaults to `None` in which case all load combinations will be added to the list of load combinations to be run, unless `combo_tags` is not `None`.
:type load_combos: list, optional
:return: A list containing the load combinations to be analyzed.
:rtype: list
"""

# Identify which load combinations to evaluate
if combo_tags is None:
if combo_tags is None and load_combos is None:
# Evaluate all load combinations if not tags have been provided
combo_list = model.LoadCombos.values()
else:
# Initialize the list of load combinations to be evaluated
combo_list = []
# Step through each load combination in the model
for combo in model.LoadCombos.values():
# Check if this load combination is tagged with any of the tags we're looking for
# Check if this load combination is tagged with any of the tags we're looking for or if it is named in the list of load combinations to be evaluated
if combo.combo_tags is not None and any(tag in combo.combo_tags for tag in combo_tags):
# Add the load combination to the list of load combinations to be evaluated
combo_list.append(combo)

elif load_combos is not None and combo.name in load_combos:
# Add the load combination by its name to the list of load combinations to be evaluated
combo_list.append(combo)

# Return the list of load combinations to be evaluated
return combo_list


def _check_stability(model, K):
"""
Identifies nodal instabilities in a model's stiffness matrix.
Expand Down Expand Up @@ -611,7 +618,7 @@ def _check_TC_convergence(model, combo_name='Combo 1', log=True):
# Return whether the TC analysis has converged
return convergence

def _calc_reactions(model, log=False, combo_tags=None):
def _calc_reactions(model, log=False, combo_tags:list=None,load_combos:list=None):
"""
Calculates reactions internally once the model is solved.

Expand All @@ -625,7 +632,7 @@ def _calc_reactions(model, log=False, combo_tags=None):
if log: print('- Calculating reactions')

# Identify which load combinations to evaluate
combo_list = _identify_combos(model, combo_tags)
combo_list = _identify_combos(model, combo_tags,load_combos)

# Calculate the reactions node by node
for node in model.Nodes.values():
Expand Down
70 changes: 48 additions & 22 deletions PyNite/FEModel3D.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from numpy import array, zeros, matmul, divide, subtract, atleast_2d
from numpy.linalg import solve
from scipy.spatial import KDTree

from PyNite.Node3D import Node3D
from PyNite.Material import Material
Expand Down Expand Up @@ -96,8 +97,7 @@ def LoadCases(self):
def add_node(self, name, X, Y, Z):
"""Adds a new node to the model.

:param name: A unique user-defined name for the node. If set to None or "" a name will be
automatically assigned.
:param name: A unique user-defined name for the node. If set to None or "" a name will be automatically assigned.
:type name: str
:param X: The node's global X-coordinate.
:type X: number
Expand Down Expand Up @@ -724,6 +724,25 @@ def add_cylinder_mesh(self, name, mesh_size, radius, height, thickness, material
#Return the mesh's name
return name

def determine_proximal_nodes(self, tolerance=0.001):
"""A method using a KD-Tree to efficiently determine nodes which are within a certain distance of each other"""

#TODO: add nodes that are not in the FEA structure
#TODO: add nodes that are in another structure + origin offset (for merging frames)
nodes = list(self.Nodes.keys())
data = list([(v.X,v.Y,v.Z) for v in self.Nodes.values()])

#make the tree
tree = KDTree(data)
#find pairs within distance (exact)
indexes = tree.query_pairs(r=tolerance)
out = []
#get the mode names
for i, j in indexes:
out.append((nodes[i], nodes[j]))
return out


def merge_duplicate_nodes(self, tolerance=0.001):
"""Removes duplicate nodes from the model and returns a list of the removed node names.

Expand Down Expand Up @@ -768,36 +787,38 @@ def merge_duplicate_nodes(self, tolerance=0.001):
if node_lookup[node_1_name] is None:
continue

# There is no need to check `node_1` against itself
#Check combinations. There is no need to check `node_1` against itself
for node_2_name in node_names[i + 1:]:

# Skip iteration if node_2 has already been removed
if node_lookup[node_2_name] is None:
continue

# Calculate the distance between nodes
if self.Nodes[node_1_name].distance(self.Nodes[node_2_name]) > tolerance:
n2 = self.Nodes[node_2_name]
n1 = self.Nodes[node_1_name]
if n1.distance(n2) > tolerance:
continue

# Replace references to `node_2` in each element with references to `node_1`
for element, node_type in node_lookup[node_2_name]:
setattr(element, node_type, self.Nodes[node_1_name])
setattr(element, node_type, n1)

# Flag `node_2` as no longer used
node_lookup[node_2_name] = None

# Merge any boundary conditions
support_cond = ('support_DX', 'support_DY', 'support_DZ', 'support_RX', 'support_RY', 'support_RZ')
for dof in support_cond:
if getattr(self.Nodes[node_2_name], dof) == True:
setattr(self.Nodes[node_1_name], dof, True)
if getattr(n2, dof) == True:
setattr(n1, dof, True)

# Merge any spring supports
spring_cond = ('spring_DX', 'spring_DY', 'spring_DZ', 'spring_RX', 'spring_RY', 'spring_RZ')
for dof in spring_cond:
value = getattr(self.Nodes[node_2_name], dof)
value = getattr(n2, dof)
if value != [None, None, None]:
setattr(self.Nodes[node_1_name], dof, value)
setattr(n1, dof, value)

# Fix the mesh labels
for mesh in self.Meshes.values():
Expand All @@ -806,17 +827,22 @@ def merge_duplicate_nodes(self, tolerance=0.001):
if node_2_name in mesh.nodes.keys():

# Attach the correct node to the mesh
mesh.nodes[node_2_name] = self.Nodes[node_1_name]
mesh.nodes[node_2_name] = n1

# Fix the dictionary key
#print(f'{mesh} rmv {node_2_name} -> {node_1_name}')
mesh.nodes[node_1_name] = mesh.nodes.pop(node_2_name)

# Fix the elements in the mesh
for element in mesh.elements.values():
if node_2_name == element.i_node.name: element.i_node = self.Nodes[node_1_name]
if node_2_name == element.j_node.name: element.j_node = self.Nodes[node_1_name]
if node_2_name == element.m_node.name: element.m_node = self.Nodes[node_1_name]
if node_2_name == element.n_node.name: element.n_node = self.Nodes[node_1_name]
if node_2_name == element.i_node.name:
element.i_node = n1
if node_2_name == element.j_node.name:
element.j_node = n1
if node_2_name == element.m_node.name:
element.m_node = n1
if node_2_name == element.n_node.name:
element.n_node = n1

# Add the node to the `remove` list
remove_list.append(node_2_name)
Expand Down Expand Up @@ -1892,7 +1918,7 @@ def D(self, combo_name='Combo 1'):
# Return the global displacement vector
return self._D[combo_name]

def analyze(self, log=False, check_stability=True, check_statics=False, max_iter=30, sparse=True, combo_tags=None):
def analyze(self, log=False, check_stability=True, check_statics=False, max_iter=30, sparse=True, combo_tags:list=None,load_combos:list=None):
"""Performs first-order static analysis. Iterations are performed if tension-only members or compression-only members are present.

:param log: Prints the analysis log to the console if set to True. Default is False.
Expand Down Expand Up @@ -1925,7 +1951,7 @@ def analyze(self, log=False, check_stability=True, check_statics=False, max_iter
D1_indices, D2_indices, D2 = Analysis._partition_D(self)

# Identify which load combinations have the tags the user has given
combo_list = Analysis._identify_combos(self, combo_tags)
combo_list = Analysis._identify_combos(self, combo_tags,load_combos)

# Step through each load combination
for combo in combo_list:
Expand Down Expand Up @@ -2009,7 +2035,7 @@ def analyze(self, log=False, check_stability=True, check_statics=False, max_iter
# Flag the model as solved
self.solution = 'Linear TC'

def analyze_linear(self, log=False, check_stability=True, check_statics=False, sparse=True, combo_tags=None):
def analyze_linear(self, log=False, check_stability=True, check_statics=False, sparse=True, combo_tags:list=None,load_combos:list=None):
"""Performs first-order static analysis. This analysis procedure is much faster since it only assembles the global stiffness matrix once, rather than once for each load combination. It is not appropriate when non-linear behavior such as tension/compression only analysis or P-Delta analysis are required.

:param log: Prints the analysis log to the console if set to True. Default is False.
Expand Down Expand Up @@ -2047,7 +2073,7 @@ def analyze_linear(self, log=False, check_stability=True, check_statics=False, s
K11, K12, K21, K22 = Analysis._partition(self, self.K(combo_name, log, check_stability, sparse), D1_indices, D2_indices)

# Identify which load combinations have the tags the user has given
combo_list = Analysis._identify_combos(self, combo_tags)
combo_list = Analysis._identify_combos(self, combo_tags,load_combos)

# Step through each load combination
for combo in combo_list:
Expand Down Expand Up @@ -2101,7 +2127,7 @@ def analyze_linear(self, log=False, check_stability=True, check_statics=False, s
# Flag the model as solved
self.solution = 'Linear'

def analyze_PDelta(self, log=False, check_stability=True, max_iter=30, sparse=True, combo_tags=None):
def analyze_PDelta(self, log=False, check_stability=True, max_iter=30, sparse=True, combo_tags:list=None,load_combos:list=None):
"""Performs second order (P-Delta) analysis. This type of analysis is appropriate for most models using beams, columns and braces. Second order analysis is usually required by material specific codes. The analysis is iterative and takes longer to solve. Models with slender members and/or members with combined bending and axial loads will generally have more significant P-Delta effects. P-Delta effects in plates/quads are not considered.

:param log: Prints updates to the console if set to True. Default is False.
Expand Down Expand Up @@ -2132,7 +2158,7 @@ def analyze_PDelta(self, log=False, check_stability=True, max_iter=30, sparse=Tr
D1_indices, D2_indices, D2 = Analysis._partition_D(self)

# Identify which load combinations have the tags the user has given
combo_list = Analysis._identify_combos(self, combo_tags)
combo_list = Analysis._identify_combos(self, combo_tags,load_combos)

# Step through each load combination
for combo in combo_list:
Expand All @@ -2157,7 +2183,7 @@ def analyze_PDelta(self, log=False, check_stability=True, max_iter=30, sparse=Tr
# Flag the model as solved
self.solution = 'P-Delta'

def _not_ready_yet_analyze_pushover(self, log=False, check_stability=True, push_combo='Push', max_iter=30, tol=0.01, sparse=True, combo_tags=None):
def _not_ready_yet_analyze_pushover(self, log=False, check_stability=True, push_combo='Push', max_iter=30, tol=0.01, sparse=True, combo_tags:list=None,load_combos:list=None):

if log:
print('+---------------------+')
Expand Down Expand Up @@ -2188,7 +2214,7 @@ def _not_ready_yet_analyze_pushover(self, log=False, check_stability=True, push_

# Identify which load combinations have the tags the user has given
# TODO: Remove the pushover combo istelf from `combo_list`
combo_list = Analysis._identify_combos(self, combo_tags)
combo_list = Analysis._identify_combos(self, combo_tags,load_combos)
combo_list = [combo for combo in combo_list if combo.name != push_combo]

# Step through each load combination
Expand Down
Loading