Source code for anastruct.fem.system

import math, re, collections, copy
import numpy as np
from anastruct.basic import FEMException, args_to_lists
from anastruct.fem.postprocess import SystemLevel as post_sl
from anastruct.fem.elements import Element
from anastruct.vertex import Vertex
from anastruct.fem import plotter
from . import system_components
from anastruct.vertex import vertex_range


[docs]class SystemElements: """ Modelling any structure starts with an object of this class. :ivar EA: Standard axial stiffness of elements, default=15,000 :ivar EI: Standard bending stiffness of elements, default=5,000 :ivar figsize: (tpl) Matplotlibs standard figure size :ivar element_map: (dict) Keys are the element ids, values are the element objects :ivar node_map: (dict) Keys are the node ids, values are the node objects. :ivar node_element_map: (dict) maps node ids to element objects. :ivar supports_fixed: (list) All the fixed supports in the system. :ivar supports_hinged: (list) All the hinged supports in the system. :ivar supports_roll: (list) All the roll supports in the system. :ivar supports_spring_x: (list) All the spring supports in x-direction in the system. :ivar supports_spring_z: (list) All the spring supports in z-direction in the system. :ivar supports_spring_y: (list) All the spring supports in y-direction in the system. :ivar supports_roll_direction: (list) The directions of the rolling supports. :ivar loads_point: (dict) Maps node ids to point loads. :ivar loads_q: (dict) Maps element ids to q-loads. :ivar loads_moment: (dict) Maps node ids to moment loads. :ivar loads_dead_load: (set) Element ids that have a dead load applied. """
[docs] def __init__(self, figsize=(12, 8), EA=15e3, EI=5e3, load_factor=1, mesh=50): """ * E = Young's modulus * A = Area * I = Moment of Inertia :param figsize: (tpl) Set the standard plotting size. :param EA: (flt) Standard E * A. Set the standard values of EA if none provided when generating an element. :param EI: (flt) Standard E * I. Set the standard values of EA if none provided when generating an element. :param load_factor: (flt) Multiply all loads with this factor. :param mesh: (int) Plotting mesh. Has no influence on the calculation. """ # init object self.post_processor = post_sl(self) self.plotter = plotter.Plotter(self, mesh) self.plot_values = plotter.PlottingValues(self, mesh) # standard values if none provided self.EA = EA self.EI = EI self.figsize = figsize self.orientation_cs = -1 # needed for the loads directions # structure system self.element_map = {} # maps element ids to the Element objects. self.node_map = {} # maps node ids to the Node objects. self.node_element_map = {} # maps node ids to Element objects self.system_spring_map = {} # keys matrix index (for both row and columns), values K # list of indexes that remain after conditions are applied self._remainder_indexes = [] # keep track of the node_id of the supports self.supports_fixed = [] self.supports_hinged = [] self.supports_roll = [] self.supports_spring_x = [] self.supports_spring_z = [] self.supports_spring_y = [] self.supports_roll_direction = [] # save tuples of the arguments for copying purposes. self.supports_spring_args = [] # keep track of the loads self.loads_point = {} # node ids with a point loads self.loads_q = {} # element ids with a q-load self.loads_moment = {} self.loads_dead_load = set() # element ids with q-load due to dead load # results self.reaction_forces = {} # node objects self.non_linear = False self.non_linear_elements = {} # keys are element ids, values are dicts: {node_index: max moment capacity} self.buckling_factor = None # previous point of element self._previous_point = Vertex(0, 0) self.load_factor = load_factor # Objects state self.count = 0 self.system_matrix_locations = [] self.system_matrix = None self.system_force_vector = None self.system_displacement_vector = None self.shape_system_matrix = None self.reduced_force_vector = None self.reduced_system_matrix = None self._vertices = {} # maps vertices to node ids
@property def id_last_element(self): return max(self.element_map.keys()) @property def id_last_node(self): return max(self.node_map.keys())
[docs] def add_element_grid(self, x, y, EA=None, EI=None, g=None, mp=None, spring=None, **kwargs): """ Add multiple elements defined by two containers with coordinates. :param x: (list/ np.array) x coordinates. :param y: (list/ np.array) y coordinates. :param EA: See 'add_element' method :param EI: See 'add_element' method :param g: See 'add_element' method :param mp: See 'add_element' method :param spring: See 'add_element' method :paramg **kwargs: See 'add_element' method :return: None """ a = np.ones(len(x)) if EA is None: EA = a * self.EA if EI is None: EI = a * self.EI if g is None: g = a * 0 for i in range(len(x) - 1): self.add_element([[x[i], y[i]], [x[i + 1], y[i + 1]]], EA[i], EI[i], g[i], mp, spring, **kwargs)
[docs] def add_truss_element(self, location, EA=None): """ .. highlight:: python Add an element that only has axial force. :param location: (list/ Vertex) The two nodes of the element or the next node of the element. :Example: .. code-block:: python location=[[x, y], [x, y]] location=[Vertex, Vertex] location=[x, y] location=Vertex :param EA: (flt) EA :return: (int) Elements ID. """ return self.add_element(location, EA, 1e-14, element_type='truss')
[docs] def add_element(self, location, EA=None, EI=None, g=0, mp=None, spring=None, **kwargs): """ :param location: (list/ Vertex) The two nodes of the element or the next node of the element. :Example: .. code-block:: python location=[[x, y], [x, y]] location=[Vertex, Vertex] location=[x, y] location=Vertex :param EA: (flt) EA :param EI: (flt) EI :param g: (flt) Weight per meter. [kN/m] / [N/m] :param mp: (dict) Set a maximum plastic moment capacity. Keys are integers representing the nodes. Values are the bending moment capacity. :Example: .. code-block:: python mp={1: 210e3, 2: 180e3} :param spring: (dict) Set a rotational spring or a hinge (k=0) at node 1 or node 2. :Example: .. code-block:: python spring={1: k 2: k} # Set a hinged node: spring={1: 0} :return: (int) Elements ID. """ element_type = kwargs.get("element_type", "general") EA = self.EA if EA is None else EA EI = self.EI if EI is None else EI # add the element number self.count += 1 point_1, point_2 = system_components.util.det_vertices(self, location) node_id1, node_id2 = system_components.util.det_node_ids(self, point_1, point_2) point_1, point_2, node_id1, node_id2, spring, mp, ai = \ system_components.util.force_elements_orientation(point_1, point_2, node_id1, node_id2, spring, mp) system_components.util.append_node_id(self, point_1, point_2, node_id1, node_id2) system_components.util.ensure_single_hinge(self, spring, node_id1, node_id2) # add element element = Element(self.count, EA, EI, (point_2 - point_1).modulus(), ai, point_1, point_2, spring) element.node_id1 = node_id1 element.node_id2 = node_id2 element.node_map = {node_id1: self.node_map[node_id1], node_id2: self.node_map[node_id2]} element.type = element_type self.element_map[self.count] = element for node in (node_id1, node_id2): if node in self.node_element_map: self.node_element_map[node].append(element) else: self.node_element_map[node] = [element] # Register the elements per node for node_id in (node_id1, node_id2): self.node_map[node_id].elements[element.id] = element if mp is not None: assert type(mp) == dict, "The mp parameter should be a dictionary." self.non_linear_elements[element.id] = mp self.non_linear = True system_components.assembly.dead_load(self, g, element.id) return self.count
[docs] def add_multiple_elements(self, location, n=None, dl=None, EA=None, EI=None, g=0, mp=None, spring=None, **kwargs): """ Add multiple elements defined by the first and the last point. :param location: See 'add_element' method :param n: (int) Number of elements. :param dl: (flt) Distance between the elements nodes. :param EA: See 'add_element' method :param EI: See 'add_element' method :param g: See 'add_element' method :param mp: See 'add_element' method :param spring: See 'add_element' method **Keyword Args:** :param element_type: (str) See 'add_element' method :param first: (dict) Different arguments for the first element :param last: (dict) Different arguments for the last element :Example: .. code-block:: python last={'EA': 1e3, 'mp': 290} :return: (list) Element IDs """ first = kwargs.get("first", {}) last = kwargs.get("last", {}) element_type = kwargs.get("element_type", "general") for el in (first, last): if "EA" not in el: el["EA"] = EA if "EI" not in el: el["EI"] = EI if "g" not in el: el["g"] = g if "mp" not in el: el["mp"] = mp if "spring" not in el: el["spring"] = spring if "element_type" not in el: el["element_type"] = element_type point_1, point_2 = system_components.util.det_vertices(self, location) length = (point_2 - point_1).modulus() direction = (point_2 - point_1).unit() if type(n) == type(dl): raise FEMException("Wrong parameters", "One, and only one, of n and dl should be passed as argument.") elif n: dl = length / n point = point_1 + direction * dl elements = [ self.add_element((point_1, point), first["EA"], first["EI"], first["g"], first["mp"], first["spring"], element_type=first["element_type"])] l = 2 * dl while l < length: point += direction * dl elements.append(self.add_element(point, EA, EI, g, mp, spring, element_type=element_type)) l += dl elements.append(self.add_element(point_2, last["EA"], last["EI"], last["g"], last["mp"], last["spring"], element_type=last["element_type"])) return elements
[docs] def insert_node(self, element_id, location=None, factor=None): """ Insert a node into an existing structure. This can be done by adding a new Vertex at any given location, or by setting a factor of the elements length. E.g. if you want a node at 40% of the elements length, you pass factor = 0.4. Note: this method completely rebuilds the SystemElements object and is therefore slower then building a model with `add_element` methods. :param element_id: (int) Id number of the element you want to insert the node. :param location: (list/ Vertex) The nodes of the element or the next node of the element. :Example: .. code-block:: python location=[x, y] location=Vertex :param: factor: (flt) Value between 0 and 1 to determine the new node location. """ ss = SystemElements(EA=self.EA, EI=self.EI, load_factor=self.load_factor, mesh=self.plotter.mesh) for element in self.element_map.values(): g = self.element_map[element.id].dead_load mp = self.non_linear_elements[element.id] if element.id in self.non_linear_elements else None if element_id == element.id: if factor is not None: location = factor * (element.vertex_2 - element.vertex_1) + element.vertex_1 else: location = Vertex(location) mp1 = mp2 = spring1 = spring2 = None if mp is not None: if 1 in mp: mp1 = {1: mp[1]} if 2 in mp: mp2 = {2: mp[2]} if element.springs is not None: if 1 in element.springs: spring1 = {1: element.springs[1]} if 2 in element.springs: spring2 = {2: element.springs[2]} ss.add_element([element.vertex_1, location], EA=element.EA, EI=element.EI, g=g, mp=mp1, spring=spring1) ss.add_element([location, element.vertex_2], EA=element.EA, EI=element.EI, g=g, mp=mp2, spring=spring2) else: ss.add_element([element.vertex_1, element.vertex_2], EA=element.EA, EI=element.EI, g=g, mp=mp, spring=element.springs) self.__dict__ = ss.__dict__.copy()
[docs] def solve(self, force_linear=False, verbosity=0, max_iter=200, geometrical_non_linear=False, **kwargs): """ Compute the results of current model. :param force_linear: (bool) Force a linear calculation. Even when the system has non linear nodes. :param verbosity: (int) 0. Log calculation outputs. 1. silence. :param max_iter: (int) Maximum allowed iterations. :param geometrical_non_linear: (bool) Calculate second order effects and determine the buckling factor. :return: (array) Displacements vector. Development **kwargs: :param naked: (bool) Whether or not to run the solve function without doing post processing. :param discretize_kwargs: When doing a geometric non linear analysis you can reduce or increase the number of elements created that are used for determining the buckling_factor """ # kwargs: arguments for the iterative solver callers such as the _stiffness_adaptation method. # naked (bool) Default = False, if True force lines won't be computed. naked = kwargs.get("naked", False) if not naked: if not self.validate(): if any(['general' in element.type for element in self.element_map.values()]): raise FEMException('StabilityError', 'The eigenvalues of the stiffness matrix are non zero, ' 'which indicates a instable structure. ' 'Check your support conditions') # (Re)set force vectors for el in self.element_map.values(): el.reset() system_components.assembly.prep_matrix_forces(self) assert (self.system_force_vector is not None), "There are no forces on the structure" if self.non_linear and not force_linear: return system_components.solver.stiffness_adaptation(self, verbosity, max_iter) system_components.assembly.assemble_system_matrix(self) if geometrical_non_linear: discretize_kwargs = kwargs.get('discretize_kwargs', None) self.buckling_factor = system_components.solver.geometrically_non_linear(self, verbosity, discretize_kwargs=discretize_kwargs) return self.system_displacement_vector system_components.assembly.process_conditions(self) # solution of the reduced system (reduced due to support conditions) reduced_displacement_vector = np.linalg.solve(self.reduced_system_matrix, self.reduced_force_vector) # add the solution of the reduced system in the complete system displacement vector self.system_displacement_vector = np.zeros(self.shape_system_matrix) np.put(self.system_displacement_vector, self._remainder_indexes, reduced_displacement_vector) # determine the displacement vector of the elements for el in self.element_map.values(): index_node_1 = (el.node_1.id - 1) * 3 index_node_2 = (el.node_2.id - 1) * 3 # node 1 ux, uz, phi el.element_displacement_vector[:3] = self.system_displacement_vector[index_node_1: index_node_1 + 3] # node 2 ux, uz, phi el.element_displacement_vector[3:] = self.system_displacement_vector[index_node_2: index_node_2 + 3] el.determine_force_vector() if not naked: # determining the node results in post processing class self.post_processor.node_results_elements() self.post_processor.node_results_system() self.post_processor.reaction_forces() self.post_processor.element_results() # check the values in the displacement vector for extreme values, indicating a flawed calculation assert (np.any(self.system_displacement_vector < 1e6)), "The displacements of the structure exceed 1e6. " \ "Check your support conditions," \ "or your elements Young's modulus" return self.system_displacement_vector
def validate(self, min_eigen=1e-9): """ Validate the stability of the stiffness matrix. :param min_eigen: (flt) Minimum value of the eigenvalues of the stiffness matrix. This value should be close to zero. :return: (bool) """ ss = copy.copy(self) system_components.assembly.prep_matrix_forces(ss) assert (np.abs(ss.system_force_vector).sum() != 0), "There are no forces on the structure" ss._remainder_indexes = [] system_components.assembly.assemble_system_matrix(ss) system_components.assembly.process_conditions(ss) w, _ = np.linalg.eig(ss.reduced_system_matrix) return np.all(w > min_eigen)
[docs] def add_support_hinged(self, node_id): """ Model a hinged support at a given node. :param node_id: (int/ list) Represents the nodes ID """ if not isinstance(node_id, collections.Iterable): node_id = (node_id,) for id_ in node_id: id_ = _negative_index_to_id(id_, self.node_map.keys()) system_components.util.support_check(self, id_) system_components.assembly.set_displacement_vector(self, [(id_, 1), (id_, 2)]) # add the support to the support list for the plotter self.supports_hinged.append(self.node_map[id_])
[docs] def add_support_roll(self, node_id, direction=2): """ Adds a rolling support at a given node. :param node_id: (int/ list) Represents the nodes ID :param direction: (int/ list) Represents the direction that is fixed: x = 1, y = 2 """ if not isinstance(node_id, collections.Iterable): node_id = (node_id,) for id_ in node_id: id_ = _negative_index_to_id(id_, self.node_map.keys()) system_components.util.support_check(self, id_) system_components.assembly.set_displacement_vector(self, [(id_, direction)]) # add the support to the support list for the plotter self.supports_roll.append(self.node_map[id_]) self.supports_roll_direction.append(direction)
[docs] def add_support_fixed(self, node_id): """ Add a fixed support at a given node. :param node_id: (int/ list) Represents the nodes ID """ if not isinstance(node_id, collections.Iterable): node_id = (node_id,) for id_ in node_id: id_ = _negative_index_to_id(id_, self.node_map.keys()) system_components.util.support_check(self, id_) system_components.assembly.set_displacement_vector(self, [(id_, 1), (id_, 2), (id_, 3)]) # add the support to the support list for the plotter self.supports_fixed.append(self.node_map[id_])
[docs] def add_support_spring(self, node_id, translation, k, roll=False): """ Add a translational support at a given node. :param translation: (int/ list) Represents the prevented translation. **Note** | 1 = translation in x | 2 = translation in z | 3 = rotation in y :param node_id: (int/ list) Integer representing the nodes ID. :param k: (flt) Stiffness of the spring :param roll: (bool) If set to True, only the translation of the spring is controlled. """ self.supports_spring_args.append((node_id, translation, k, roll)) # The stiffness of the spring is added in the system matrix at the location that represents the node and the # displacement. if not isinstance(node_id, collections.Iterable): node_id = (node_id,) for id_ in node_id: id_ = _negative_index_to_id(id_, self.node_map.keys()) system_components.util.support_check(self, id_) # determine the location in the system matrix # row and column are the same matrix_index = (id_ - 1) * 3 + translation - 1 self.system_spring_map[matrix_index] = k # add the support to the support list for the plotter if translation == 1: self.supports_spring_x.append(self.node_map[id_]) elif translation == 2: self.supports_spring_z.append(self.node_map[id_]) else: self.supports_spring_y.append(self.node_map[id_]) if not roll: # fix the other d.o.f. if translation == 1: # translation spring in x-axis system_components.assembly.set_displacement_vector(self, [(id_, 2)]) elif translation == 2: # translation spring in z-axis system_components.assembly.set_displacement_vector(self, [(id_, 1)]) elif translation == 3: # rotational spring in y-axis system_components.assembly.set_displacement_vector(self, [(id_, 1), (id_, 2)])
[docs] def q_load(self, q, element_id, direction="element"): """ Apply a q-load to an element. :param element_id: (int/ list) representing the element ID :param q: (flt) value of the q-load :param direction: (str) "element", "x", "y" """ q, element_id, direction = args_to_lists(q, element_id, direction) for i in range(len(element_id)): id_ = _negative_index_to_id(element_id[i], self.element_map.keys()) self.plotter.max_q = max(self.plotter.max_q, abs(q[i])) self.loads_q[id_] = q[i] * self.orientation_cs * self.load_factor el = self.element_map[id_] el.q_load = q[i] * self.orientation_cs * self.load_factor el.q_direction = direction[i]
[docs] def point_load(self, node_id, Fx=0, Fy=0, rotation=0): """ Apply a point load to a node. :param node_id: (int/ list) Nodes ID. :param Fx: (flt/ list) Force in global x direction. :param Fy: (flt/ list) Force in global x direction. :param rotation: (flt/ list) Rotate the force clockwise. Rotation is in degrees. """ node_id, Fx, Fy, rotation = args_to_lists(node_id, Fx, Fy, rotation) for i in range(len(node_id)): id_ = _negative_index_to_id(node_id[i], self.node_map.keys()) self.plotter.max_system_point_load = max(self.plotter.max_system_point_load, (Fx[i] ** 2 + Fy[i] ** 2) ** 0.5) cos = math.cos(math.radians(rotation[i])) sin = math.sin(math.radians(rotation[i])) self.loads_point[id_] = (Fx[i] * cos + Fy[i] * sin, Fy[i] * self.orientation_cs * cos + Fx[i] * sin)
[docs] def moment_load(self, node_id, Ty): """ Apply a moment on a node. :param node_id: (int/ list) Nodes ID. :param Ty: (flt/ list) Moments acting on the node. """ node_id, Ty = args_to_lists(node_id, Ty) for i in range(len(node_id)): id_ = _negative_index_to_id(node_id[i], self.node_map.keys()) self.loads_moment[id_] = Ty[i]
[docs] def show_structure(self, verbosity=0, scale=1., offset=(0, 0), figsize=None, show=True, supports=True, values_only=False): """ Plot the structure. :param verbosity: (int) 0: All information, 1: Suppress information. :param scale: (flt) Scale of the plot. :param offset: (tpl) Offset the plots location on the figure. :param figsize: (tpl) Change the figure size. :param show: (bool) Plot the result or return a figure. :param supports: (bool) Show the supports. :param values_only: (bool) Return the values that would be plotted as tuple containing two arrays: (x, y) :return: (figure) """ figsize = self.figsize if figsize is None else figsize if values_only: return self.plot_values.structure() return self.plotter.plot_structure(figsize, verbosity, show, supports, scale, offset)
[docs] def show_bending_moment(self, factor=None, verbosity=0, scale=1, offset=(0, 0), figsize=None, show=True, values_only=False): """ Plot the bending moment. :param factor: (flt) Influence the plotting scale. :param verbosity: (int) 0: All information, 1: Suppress information. :param scale: (flt) Scale of the plot. :param offset: (tpl) Offset the plots location on the figure. :param figsize: (tpl) Change the figure size. :param show: (bool) Plot the result or return a figure. :param values_only: (bool) Return the values that would be plotted as tuple containing two arrays: (x, y) :return: (figure) """ if values_only: return self.plot_values.bending_moment(factor) figsize = self.figsize if figsize is None else figsize return self.plotter.bending_moment(factor, figsize, verbosity, scale, offset, show)
[docs] def show_axial_force(self, factor=None, verbosity=0, scale=1, offset=(0, 0), figsize=None, show=True, values_only=False): """ Plot the axial force. :param factor: (flt) Influence the plotting scale. :param verbosity: (int) 0: All information, 1: Suppress information. :param scale: (flt) Scale of the plot. :param offset: (tpl) Offset the plots location on the figure. :param figsize: (tpl) Change the figure size. :param show: (bool) Plot the result or return a figure. :param values_only: (bool) Return the values that would be plotted as tuple containing two arrays: (x, y) :return: (figure) """ if values_only: return self.plot_values.axial_force(factor) figsize = self.figsize if figsize is None else figsize return self.plotter.axial_force(factor, figsize, verbosity, scale, offset, show)
[docs] def show_shear_force(self, factor=None, verbosity=0, scale=1, offset=(0, 0), figsize=None, show=True, values_only=False): """ Plot the shear force. :param factor: (flt) Influence the plotting scale. :param verbosity: (int) 0: All information, 1: Suppress information. :param scale: (flt) Scale of the plot. :param offset: (tpl) Offset the plots location on the figure. :param figsize: (tpl) Change the figure size. :param show: (bool) Plot the result or return a figure. :param values_only: (bool) Return the values that would be plotted as tuple containing two arrays: (x, y) :return: (figure) """ if values_only: return self.plot_values.shear_force(factor) figsize = self.figsize if figsize is None else figsize return self.plotter.shear_force(factor, figsize, verbosity, scale, offset, show)
[docs] def show_reaction_force(self, verbosity=0, scale=1, offset=(0, 0), figsize=None, show=True): """ Plot the reaction force. :param verbosity: (int) 0: All information, 1: Suppress information. :param scale: (flt) Scale of the plot. :param offset: (tpl) Offset the plots location on the figure. :param figsize: (tpl) Change the figure size. :param show: (bool) Plot the result or return a figure. :return: (figure) """ figsize = self.figsize if figsize is None else figsize return self.plotter.reaction_force(figsize, verbosity, scale, offset, show)
[docs] def show_displacement(self, factor=None, verbosity=0, scale=1, offset=(0, 0), figsize=None, show=True, linear=False, values_only=False): """ Plot the displacement. :param factor: (flt) Influence the plotting scale. :param verbosity: (int) 0: All information, 1: Suppress information. :param scale: (flt) Scale of the plot. :param offset: (tpl) Offset the plots location on the figure. :param figsize: (tpl) Change the figure size. :param show: (bool) Plot the result or return a figure. :param linear: (bool) Don't evaluate the displacement values in between the elements :param values_only: (bool) Return the values that would be plotted as tuple containing two arrays: (x, y) :return: (figure) """ if values_only: return self.plot_values.displacements(factor, linear) figsize = self.figsize if figsize is None else figsize return self.plotter.displacements(factor, figsize, verbosity, scale, offset, show, linear)
def show_results(self, verbosity=0, scale=1, offset=(0, 0), figsize=None, show=True): """ Plot all the results in one window. :param verbosity: (int) 0: All information, 1: Suppress information. :param scale: (flt) Scale of the plot. :param offset: (tpl) Offset the plots location on the figure. :param figsize: (tpl) Change the figure size. :param show: (bool) Plot the result or return a figure. :return: (figure) """ figsize = self.figsize if figsize is None else figsize return self.plotter.results_plot(figsize, verbosity, scale, offset, show)
[docs] def get_node_results_system(self, node_id=0): """ These are the node results. These are the opposite of the forces and displacements working on the elements and may seem counter intuitive. :param node_id: (integer) representing the node's ID. If integer = 0, the results of all nodes are returned :return: | if node_id == 0: (list) | | Returns a list containing tuples with the results: :: [(id, Fx, Fy, Ty, ux, uy, phi_y), (id, Fx, Fy...), () .. ] | | if node_id > 0: (dict) """ result_list = [] if node_id != 0: node_id = _negative_index_to_id(node_id, self.node_map) node = self.node_map[node_id] return { "id": node.id, "Fx": node.Fx, "Fy": node.Fy, "Ty": node.Ty, "ux": node.ux, "uy": -node.uz, "phi_y": node.phi_y } else: for node in self.node_map.values(): result_list.append((node.id, node.Fx, node.Fy, node.Ty, node.ux, -node.uz, node.phi_y)) return result_list
[docs] def get_node_displacements(self, node_id=0): """ :param node_id: (int) Represents the node's ID. If integer = 0, the results of all nodes are returned. :return: | if node_id == 0: (list) | | Returns a list containing tuples with the results: :: [(id, ux, uy, phi_y), (id, ux, uy, phi_y), ... (id, ux, uy, phi_y) ] | | if node_id > 0: (dict) """ result_list = [] if node_id != 0: node = self.node_map[node_id] return { "id": node.id, "ux": -node.ux, "uy": node.uz, # - * - = + "phi_y": node.phi_y } else: for node in self.node_map.values(): result_list.append((node.id, -node.ux, node.uz, node.phi_y)) return result_list
[docs] def get_element_results(self, element_id=0, verbose=False): """ :param element_id: (int) representing the elements ID. If elementID = 0 the results of all elements are returned. :param verbose: (bool) If set to True the numerical results for the deflection and the bending moments are returned. :return: | | if node_id == 0: (list) | | Returns a list containing tuples with the results: :: [(id, length, alpha, u, N_1, N_2), (id, length, alpha, u, N_1, N_2), ... (id, length, alpha, u, N_1, N_2)] | | if node_id > 0: (dict) | """ if element_id != 0: element_id = _negative_index_to_id(element_id, self.element_map) el = self.element_map[element_id] if el.type == "truss": return { "id": el.id, "length": el.l, "alpha": el.ai, "u": el.extension[0], "N": el.N_1, } else: return { "id": el.id, "length": el.l, "alpha": el.ai, "u": el.extension[0], "N": el.N_1, "wmax": np.min(el.deflection), "wmin": np.max(el.deflection), "w": el.deflection if verbose else None, "Mmin": np.min(el.bending_moment), "Mmax": np.max(el.bending_moment), "M": el.bending_moment if verbose else None, "q": el.q_load } else: result_list = [] for el in self.element_map.values(): if el.type == "truss": result_list.append({ "id": el.id, "length": el.l, "alpha": el.ai, "u": el.extension[0], "N": el.N_1, } ) else: result_list.append( { "id": el.id, "length": el.l, "alpha": el.ai, "u": el.extension[0], "N": el.N_1, "wmax": np.min(el.deflection), "wmin": np.max(el.deflection), "w": el.deflection if verbose else None, "Mmin": np.min(el.bending_moment), "Mmax": np.max(el.bending_moment), "M": el.bending_moment if verbose else None, "q": el.q_load } ) return result_list
[docs] def get_element_result_range(self, unit): """ Useful when added lots of elements. Returns a list of all the queried unit. :param unit: (str) - 'shear' - 'moment' - 'axial' :return: (list) """ if unit == "shear": return [el.shear_force[0] for el in self.element_map.values()] elif unit == "moment": return [el.bending_moment[0] for el in self.element_map.values()] elif unit == "axial": return [el.N_1 for el in self.element_map.values()]
[docs] def get_node_result_range(self, unit): """ Query a list with node results. :param unit: (str) - 'uy' - 'ux' - 'phi_y' :return: (list) """ if unit == "uy": return [node.uz for node in self.node_map.values()] # - * - = + elif unit == "ux": return [-node.ux for node in self.node_map.values()] elif unit == "phi_y": return [node.phi_y for node in self.node_map.values()]
[docs] def find_node_id(self, vertex): """ Retrieve the ID of a certain location. :param vertex: (Vertex/ list/ tpl) Vertex_xz, [x, y], (x, y) :return: (int/ None) id of the node at the location of the vertex """ if isinstance(vertex, (list, tuple)): vertex = Vertex(vertex) try: tol = 1e-9 return next(filter(lambda x: math.isclose(x.vertex.x, vertex.x, abs_tol=tol) and math.isclose(x.vertex.y, vertex.y, abs_tol=tol), self.node_map.values())).id except StopIteration: return None
[docs] def nodes_range(self, dimension): """ Retrieve a list with coordinates x or z (y). :param dimension: (str) "both", 'x', 'y' or 'z' :return: (list) """ return list( map( lambda x: x.vertex.x if dimension == 'x' else x.vertex.z if dimension == 'z' else x.vertex.y if dimension == 'y' else (x.vertex.x, x.vertex.y) if dimension == "both" else None, self.node_map.values()))
[docs] def nearest_node(self, dimension, val): """ Retrieve the nearest node ID. :param dimension: (str) "both", 'x', 'y' or 'z' :param val: (flt) Value of the dimension. :return: (int) ID of the node. """ if dimension == "both": match = list(map(lambda x: x[1], filter(lambda x: x[0][0] == val[0] and x[0][1] == val[1], zip(self.nodes_range("both"), self.node_map.keys()) ) ) ) return match[0] if len(match) > 0 else None else: return np.argmin(np.abs(np.array(self.nodes_range(dimension)) - val))
[docs] def discretize(self, n=10): """ Takes an already defined :class:`.SystemElements` object and increases the number of elements. :param n: (int) Divide the elements into n sub-elements. """ ss = SystemElements(EA=self.EA, EI=self.EI, load_factor=self.load_factor, mesh=self.plotter.mesh) for element in self.element_map.values(): g = self.element_map[element.id].dead_load mp = self.non_linear_elements[element.id] if element.id in self.non_linear_elements else None for i, v in enumerate(vertex_range(element.vertex_1, element.vertex_2, n)): if i == 0: last_v = v continue loc = [last_v, v] ss.add_element(loc, EA=element.EA, EI=element.EI, g=g, mp=mp, spring=element.springs) last_v = v # supports for node, direction in zip(self.supports_roll, self.supports_roll_direction): ss.add_support_roll((node.id - 1) * n + 1, direction) for node in self.supports_fixed: ss.add_support_fixed((node.id - 1) * n + 1) for node in self.supports_hinged: ss.add_support_hinged((node.id - 1) * n + 1) for args in self.supports_spring_args: ss.add_support_spring((args[0] - 1) * n + 1, *args[1:]) # loads for node_id, forces in self.loads_point.items(): ss.point_load((node_id - 1) * n + 1, Fx=forces[0], Fy=forces[1] / self.orientation_cs) for node_id, forces in self.loads_moment.items(): ss.moment_load((node_id - 1) * n + 1, forces) for element_id, forces in self.loads_q.items(): ss.q_load(q=forces / self.orientation_cs / self.load_factor, element_id=element_id, direction=self.element_map[element_id].q_direction) self.__dict__ = ss.__dict__.copy()
[docs] def remove_loads(self, dead_load=False): """ Remove all the applied loads from the structure. :param dead_load: (bool) Remove the dead load. """ self.loads_point = {} self.loads_q = {} self.loads_moment = {} for k in self.element_map: self.element_map[k].q_load = 0 if dead_load: self.element_map[k].dead_load = 0 if dead_load: self.loads_dead_load = set()
def apply_load_case(self, loadcase): """ :param loadcase: :return: """ for method, kwargs in loadcase.spec.items(): method = method.split('-')[0] kwargs = re.sub(r"[{}]", '', str(kwargs)) # pass the groups that match back to the replace kwargs = re.sub(r".??(\w+).?:", r'\1=', kwargs) exec('self.{}({})'.format(method, kwargs)) def __deepcopy__(self, memo): system = copy.copy(self) mesh = self.plotter.mesh system.plotter = None system.post_processor = None system.plot_values = None system.__dict__ = copy.deepcopy(system.__dict__) system.plotter = plotter.Plotter(system, mesh) system.post_processor = post_sl(system) system.plot_values = plotter.PlottingValues return system
def _negative_index_to_id(idx, collection): if idx > 0: return idx else: return max(collection) + (idx + 1)