Hello World!
""" + # ============================================================================== # linear elastic # ============================================================================== -@extend_docstring(_Material) -class ElasticOrthotropic(_Material): - """ - ElasticOrthotropic material - =========================== - Elastic, orthotropic and homogeneous material - Additional paramenters and attributes: + +class ElasticOrthotropic(Material): + """Elastic, orthotropic and homogeneous material. Parameters ---------- @@ -162,15 +147,26 @@ def __str__(self): Gxy : {} Gyz : {} Gzx : {} -""".format(self.__class__.__name__, len(self.__class__.__name__) * '-', - self.name, self.density, self.expansion, - self.Ex, self.Ey, self.Ez, self.vxy, self.vyz, self.vzx, self.Gxy, self.Gyz, self.Gzx) - -@extend_docstring(_Material) -class ElasticIsotropic(_Material): - """ - Elastic, isotropic and homogeneous material - =========================================== +""".format( + self.__class__.__name__, + len(self.__class__.__name__) * "-", + self.name, + self.density, + self.expansion, + self.Ex, + self.Ey, + self.Ez, + self.vxy, + self.vyz, + self.vzx, + self.Gxy, + self.Gyz, + self.Gzx, + ) + + +class ElasticIsotropic(Material): + """Elastic, isotropic and homogeneous material Parameters ---------- @@ -187,6 +183,7 @@ class ElasticIsotropic(_Material): Poisson's ratio v. G : float Shear modulus (automatically computed from E and v) + """ def __init__(self, *, E, v, density, expansion=None, name=None, **kwargs): @@ -205,15 +202,18 @@ def __str__(self): E : {} v : {} G : {} -""".format(self.name, self.density, self.expansion, self.E, self.v, self.G) +""".format( + self.name, self.density, self.expansion, self.E, self.v, self.G + ) @property def G(self): return 0.5 * self.E / (1 + self.v) -class Stiff(_Material): - """Elastic, very stiff and massless material. - """ + +class Stiff(Material): + """Elastic, very stiff and massless material.""" + def __init__(self, *, density, expansion=None, name=None, **kwargs): raise NotImplementedError() @@ -221,15 +221,10 @@ def __init__(self, *, density, expansion=None, name=None, **kwargs): # ============================================================================== # non-linear general # ============================================================================== -@extend_docstring(_Material) -class ElasticPlastic(ElasticIsotropic): - """ - ElasticPlastic - ============== - Elastic and plastic, isotropic and homogeneous material. - Additional parameters and attributes. +class ElasticPlastic(ElasticIsotropic): + """Elastic and plastic, isotropic and homogeneous material. Parameters ---------- @@ -271,18 +266,22 @@ def __str__(self): G : {} strain_stress : {} -""".format(self.name, self.density, self.expansion, self.E, self.v, self.G, self.strain_stress) +""".format( + self.name, self.density, self.expansion, self.E, self.v, self.G, self.strain_stress + ) # ============================================================================== # User-defined Materials # ============================================================================== + + class UserMaterial(FEAData): - """ User Defined Material. Tho implement this type of material, a + """User Defined Material. Tho implement this type of material, a separate subroutine is required """ def __init__(self, name=None, **kwargs): super(UserMaterial, self).__init__(self, name=name, **kwargs) - raise NotImplementedError('This class is not available for the selected backend plugin') + raise NotImplementedError("This class is not available for the selected backend plugin") diff --git a/src/compas_fea2/model/materials/steel.py b/src/compas_fea2/model/materials/steel.py index 561e30b48..0a0eeecb5 100644 --- a/src/compas_fea2/model/materials/steel.py +++ b/src/compas_fea2/model/materials/steel.py @@ -3,16 +3,14 @@ from __future__ import print_function from compas_fea2 import units -from .material import _Material, ElasticIsotropic +from .material import ElasticIsotropic class Steel(ElasticIsotropic): """Bi-linear steel with given yield stress. - """ - __doc__ += _Material.__doc__ - __doc__ += """ - Additional Parameters - --------------------- + + Parameters + ---------- E : float Young's modulus E. v : float @@ -43,8 +41,8 @@ class Steel(ElasticIsotropic): """ - def __init__(self, *, fy, fu, eu, E, v, density, name=None, **kwargs): - super(Steel, self).__init__(E=E, v=v, density=density, name=name, **kwargs) + def __init__(self, *, fy, fu, eu, E, v, density, **kwargs): + super(Steel, self).__init__(E=E, v=v, density=density, **kwargs) fu = fu or fy @@ -65,8 +63,8 @@ def __init__(self, *, fy, fu, eu, E, v, density, name=None, **kwargs): self.ep = ep self.E = E self.v = v - self.tension = {'f': f, 'e': e} - self.compression = {'f': fc, 'e': ec} + self.tension = {"f": f, "e": e} + self.compression = {"f": fc, "e": ec} def __str__(self): return """ @@ -82,17 +80,19 @@ def __str__(self): v : {:.2f} eu : {:.2f} ep : {:.2f} -""".format(self.name, - (self.density * units['kg/m**2']), - (self.E * units.pascal).to('GPa'), - (self.G * units.pascal).to('GPa'), - (self.fy * units.pascal).to('MPa'), - (self.fu * units.pascal).to('MPa'), - (self.v * units.dimensionless), - (self.eu * units.dimensionless), - (self.ep * units.dimensionless)) +""".format( + self.name, + (self.density * units["kg/m**2"]), + (self.E * units.pascal).to("GPa"), + (self.G * units.pascal).to("GPa"), + (self.fy * units.pascal).to("MPa"), + (self.fu * units.pascal).to("MPa"), + (self.v * units.dimensionless), + (self.eu * units.dimensionless), + (self.ep * units.dimensionless), + ) - #TODO check values and make unit independent + # TODO check values and make unit independent @classmethod def S355(cls): """Steel S355. diff --git a/src/compas_fea2/model/model.py b/src/compas_fea2/model/model.py index 20c092ea3..3bf4d3abc 100644 --- a/src/compas_fea2/model/model.py +++ b/src/compas_fea2/model/model.py @@ -5,37 +5,30 @@ import importlib import gc import pathlib -from typing import Callable, Iterable, Type -import pint from itertools import groupby -from pathlib import Path, PurePath +from pathlib import Path import os import pickle import compas_fea2 from compas_fea2.utilities._utils import timer -from compas_fea2.utilities._utils import part_method, get_docstring, problem_method, extend_docstring + +# from compas_fea2.utilities._utils import part_method, get_docstring, problem_method from compas_fea2.base import FEAData -from compas_fea2.model.parts import _Part, DeformablePart, RigidPart +from compas_fea2.model.parts import Part, RigidPart from compas_fea2.model.nodes import Node -from compas_fea2.model.elements import _Element -from compas_fea2.model.bcs import _BoundaryCondition -from compas_fea2.model.ics import _InitialCondition, InitialStressField -from compas_fea2.model.groups import _Group, NodesGroup, PartsGroup, ElementsGroup, FacesGroup -from compas_fea2.model.constraints import _Constraint, TieMPC, BeamMPC - +from compas_fea2.model.elements import Element +from compas_fea2.model.bcs import BoundaryCondition +from compas_fea2.model.ics import InitialCondition +from compas_fea2.model.groups import Group, NodesGroup, PartsGroup, ElementsGroup -from compas_fea2.units import units class Model(FEAData): """Class representing an FEA model. Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. description : str, optional Some description of the model, by default ``None``. This will be added to the input file and can be useful for future reference. @@ -45,9 +38,6 @@ class Model(FEAData): Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. description : str Some description of the model. This will be added to the input file and can be useful for future reference. @@ -66,7 +56,7 @@ class Model(FEAData): The constraints of the model. partgroups : Set[:class:`compas_fea2.model.PartsGroup`] The part groups of the model. - materials : Set[:class:`compas_fea2.model.materials._Material] + materials : Set[:class:`compas_fea2.model.materials.Material] The materials assigned in the model. sections : Set[:class:`compas_fea2.model._Section] The sections assigned in the model. @@ -74,10 +64,11 @@ class Model(FEAData): The problems added to the model. path : ::class::`pathlib.Path` Path to the main folder where the problems' results are stored. + """ - def __init__(self, *, name=None, description=None, author=None, **kwargs): - super(Model, self).__init__(name=name, **kwargs) + def __init__(self, description=None, author=None, **kwargs): + super(Model, self).__init__(**kwargs) self.description = description self.author = author self._parts = set() @@ -137,36 +128,37 @@ def loads(self): @property def path(self): return self._path + @path.setter def path(self, value): if not isinstance(value, Path): try: value = Path(value) - except: - raise ValueError('the path provided is not valid.') + except Exception: + raise ValueError("the path provided is not valid.") self._path = value.joinpath(self.name) @property def nodes(self): - node_set=set() + node_set = set() for part in self.parts: node_set.update(part.nodes) return node_set @property def elements(self): - element_set=set() + element_set = set() for part in self.parts: element_set.update(part.elements) return element_set + # ========================================================================= # Constructor methods # ========================================================================= @staticmethod - @timer(message='Model loaded from cfm file in ') + @timer(message="Model loaded from cfm file in ") def from_cfm(path): - # type: (str) -> Model """Imports a Problem object from an .cfm file through Pickle. Parameters @@ -178,19 +170,20 @@ def from_cfm(path): ------- :class:`compas_fea2.model.Model` The imported model. + """ - with open(path, 'rb') as f: + with open(path, "rb") as f: try: # disable garbage collector gc.disable() model = pickle.load(f) # enable garbage collector again gc.enable() - except: + except Exception: gc.enable() - raise RuntimeError('Model not created!') + raise RuntimeError("Model not created!") model.path = os.sep.join(os.path.split(path)[0].split(os.sep)[:-1]) - #check if the problems' results are stored in the same location + # check if the problems' results are stored in the same location for problem in model.problems: if not os.path.exists(os.path.join(model.path, problem.name)): print(f"WARNING! - Problem {problem.name} results not found! move the results folder in {model.path}") @@ -207,7 +200,6 @@ def to_json(self): raise NotImplementedError() def to_cfm(self, path): - # type: (Path) -> None """Exports the Model object to an .cfm file through Pickle. Parameters @@ -218,22 +210,22 @@ def to_cfm(self, path): Returns ------- None + """ if not isinstance(path, Path): path = Path(path) - if not path.suffix == '.cfm': + if not path.suffix == ".cfm": raise ValueError("Please provide a valid path including the name of the file.") pathlib.Path(path.parent.absolute()).mkdir(parents=True, exist_ok=True) - with open(path, 'wb') as f: + with open(path, "wb") as f: pickle.dump(self, f) - print('Model saved to: {}'.format(path)) + print("Model saved to: {}".format(path)) # ========================================================================= # Parts methods # ========================================================================= def find_part_by_name(self, name, casefold=False): - # type: (str, bool) -> DeformablePart """Find if there is a part with a given name in the model. Parameters @@ -255,7 +247,6 @@ def find_part_by_name(self, name, casefold=False): return part def contains_part(self, part): - # type: (DeformablePart) -> DeformablePart """Verify that the model contains a specific part. Parameters @@ -270,16 +261,15 @@ def contains_part(self, part): return part in self.parts def add_part(self, part): - # type: (DeformablePart) -> DeformablePart """Adds a DeformablePart to the Model. Parameters ---------- - part : :class:`compas_fea2.model._Part` + part : :class:`compas_fea2.model.Part` Returns ------- - :class:`compas_fea2.model._Part` + :class:`compas_fea2.model.Part` Raises ------ @@ -287,7 +277,7 @@ def add_part(self, part): If the part is not a part. """ - if not isinstance(part, _Part): + if not isinstance(part, Part): raise TypeError("{!r} is not a part.".format(part)) if self.contains_part(part): @@ -314,7 +304,6 @@ def add_part(self, part): return part def add_parts(self, parts): - # type: (list) -> list """Add multiple parts to the model. Parameters @@ -332,58 +321,58 @@ def add_parts(self, parts): # Nodes methods # ========================================================================= - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_node_by_key(self, key): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_by_name(self, name): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_by_location(self, point, distance, plane=None): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_closest_nodes_to_point(self, point, distance, number_of_nodes=1, plane=None): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_around_node(self, node, distance): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_closest_nodes_to_node(self, node, distance, number_of_nodes=1, plane=None): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_by_attribute(self, attr, value, tolerance): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_on_plane(self, plane): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_in_polygon(self, polygon, tolerance=1.1): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_where(self, conditions): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def contains_node(self, node): pass @@ -391,19 +380,20 @@ def contains_node(self, node): # Nodes methods # ========================================================================= - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_element_by_key(self, key): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_elements_by_name(self, name): pass # ========================================================================= # Groups methods # ========================================================================= + def add_parts_group(self, group): """Add a PartsGroup object to the Model. @@ -416,9 +406,10 @@ def add_parts_group(self, group): ------- :class:`compas_fea2.model.PartsGroup` The added group. + """ if not isinstance(group, PartsGroup): - raise TypeError('Only PartsGroups can be added to a model') + raise TypeError("Only PartsGroups can be added to a model") self.partgroups.add(group) group._registration = self # FIXME wrong because the members of the group might have a different registation return group @@ -435,6 +426,7 @@ def add_parts_groups(self, groups): ------- [:class:`compas_fea2.model.PartsGroup`] The list with the added groups. + """ return [self.add_parts_group(group) for group in groups] @@ -452,66 +444,75 @@ def group_parts_where(self, attr, value): ------- :class:`compas_fea2.model.PartsGroup` The group with the matching parts. + """ return self.add_parts_group(PartsGroup(parts=set(filter(lambda p: getattr(p, attr) == value), self.parts))) - # ========================================================================= # BCs methods # ========================================================================= - def add_bcs(self, bc, nodes, axes='global'): - # type: (_BoundaryCondition, Node, str) -> _BoundaryCondition - """Add a :class:`compas_fea2.model._BoundaryCondition` to the model. - - Note - ---- - Currently global axes are used in the Boundary Conditions definition. + def add_bcs(self, bc, nodes, axes="global"): + """Add a :class:`compas_fea2.model.BoundaryCondition` to the model. Parameters ---------- - bc : :class:`compas_fea2.model._BoundaryCondition` + bc : :class:`compas_fea2.model.BoundaryCondition` Boundary condition object to add to the model. nodes : list[:class:`compas_fea2.model.Node`] or :class:`compas_fea2.model.NodesGroup` List or Group with the nodes where the boundary condition is assigned. Returns ------- - :class:`compas_fea2.model._BoundaryCondition` + :class:`compas_fea2.model.BoundaryCondition` + + Notes + ----- + Currently global axes are used in the Boundary Conditions definition. """ - if isinstance(nodes, _Group): + if isinstance(nodes, Group): nodes = nodes._members if isinstance(nodes, Node): nodes = [nodes] - if not isinstance(bc, _BoundaryCondition): - raise TypeError('{!r} is not a Boundary Condition.'.format(bc)) + if not isinstance(bc, BoundaryCondition): + raise TypeError("{!r} is not a Boundary Condition.".format(bc)) for node in nodes: if not isinstance(node, Node): - raise TypeError('{!r} is not a Node.'.format(node)) + raise TypeError("{!r} is not a Node.".format(node)) if not node.part: - raise ValueError('{!r} is not registered to any part.'.format(node)) - elif not node.part in self.parts: - raise ValueError('{!r} belongs to a part not registered to this model.'.format(node)) + raise ValueError("{!r} is not registered to any part.".format(node)) + elif node.part not in self.parts: + raise ValueError("{!r} belongs to a part not registered to this model.".format(node)) if isinstance(node.part, RigidPart): if len(nodes) != 1 or not node.is_reference: - raise ValueError('For rigid parts bundary conditions can be assigned only to the reference point') + raise ValueError("For rigid parts bundary conditions can be assigned only to the reference point") node._bc = bc - self._bcs[bc]=set(nodes) + self._bcs[bc] = set(nodes) bc._registration = self return bc - def _add_bc_type(self, bc_type, nodes, axes='global'): - # type: (str, Node, str) -> _BoundaryCondition - """Add a :class:`compas_fea2.model._BoundaryCondition` by type. + def _add_bc_type(self, bc_type, nodes, axes="global"): + """Add a :class:`compas_fea2.model.BoundaryCondition` by type. - Note - ---- + Parameters + ---------- + name : str + name of the boundary condition + bc_type : str + one of the boundary condition types specified above + nodes : list[:class:`compas_fea2.model.Node`] or :class:`compas_fea2.model.NodesGroup` + List or Group with the nodes where the boundary condition is assigned. + axes : str, optional + [axes of the boundary condition, by default 'global' + + Notes + ----- The bc_type must be one of the following: .. csv-table:: @@ -529,29 +530,25 @@ def _add_bc_type(self, bc_type, nodes, axes='global'): rollerYZ, :class:`compas_fea2.model.bcs.RollerBCYZ` rollerXZ, :class:`compas_fea2.model.bcs.RollerBCXZ` - - Parameters - ---------- - name : str - name of the boundary condition - bc_type : str - one of the boundary condition types specified above - nodes : list[:class:`compas_fea2.model.Node`] or :class:`compas_fea2.model.NodesGroup` - List or Group with the nodes where the boundary condition is assigned. - axes : str, optional - [axes of the boundary condition, by default 'global' """ - types = {'fix': 'FixedBC', 'fixXX': 'FixedBCXX', 'fixYY': 'FixedBCYY', - 'fixZZ': 'FixedBCZZ', 'pin': 'PinnedBC', 'rollerX': 'RollerBCX', - 'rollerY': 'RollerBCY', 'rollerZ': 'RollerBCZ', 'rollerXY': 'RollerBCXY', - 'rollerYZ': 'RollerBCYZ', 'rollerXZ': 'RollerBCXZ', - } - m = importlib.import_module('compas_fea2.model.bcs') + types = { + "fix": "FixedBC", + "fixXX": "FixedBCXX", + "fixYY": "FixedBCYY", + "fixZZ": "FixedBCZZ", + "pin": "PinnedBC", + "rollerX": "RollerBCX", + "rollerY": "RollerBCY", + "rollerZ": "RollerBCZ", + "rollerXY": "RollerBCXY", + "rollerYZ": "RollerBCYZ", + "rollerXZ": "RollerBCXZ", + } + m = importlib.import_module("compas_fea2.model.bcs") bc = getattr(m, types[bc_type])() return self.add_bcs(bc, nodes, axes) - def add_fix_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_fix_bc(self, nodes, axes="global"): """Add a :class:`compas_fea2.model.FixedBC` to the nodes in a part. Parameters @@ -562,11 +559,11 @@ def add_fix_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('fix', nodes, axes) + return self._add_bc_type("fix", nodes, axes) - def add_pin_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_pin_bc(self, nodes, axes="global"): """Add a pinned boundary condition type to some nodes in a part. Parameters @@ -577,11 +574,11 @@ def add_pin_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('pin', nodes, axes) + return self._add_bc_type("pin", nodes, axes) - def add_clampXX_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_clampXX_bc(self, nodes, axes="global"): """Add a fixed boundary condition type free about XX to some nodes in a part. Parameters @@ -592,11 +589,11 @@ def add_clampXX_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('clampXX', nodes, axes) + return self._add_bc_type("clampXX", nodes, axes) - def add_clampYY_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_clampYY_bc(self, nodes, axes="global"): """Add a fixed boundary condition free about YY type to some nodes in a part. Parameters @@ -607,11 +604,11 @@ def add_clampYY_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('clampYY', nodes, axes) + return self._add_bc_type("clampYY", nodes, axes) - def add_clampZZ_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_clampZZ_bc(self, nodes, axes="global"): """Add a fixed boundary condition free about ZZ type to some nodes in a part. Parameters @@ -622,11 +619,11 @@ def add_clampZZ_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('clampZZ', nodes, axes) + return self._add_bc_type("clampZZ", nodes, axes) - def add_rollerX_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerX_bc(self, nodes, axes="global"): """Add a roller free on X boundary condition type to some nodes in a part. Parameters @@ -637,11 +634,11 @@ def add_rollerX_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerX', nodes, axes) + return self._add_bc_type("rollerX", nodes, axes) - def add_rollerY_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerY_bc(self, nodes, axes="global"): """Add a roller free on Y boundary condition type to some nodes in a part. Parameters @@ -652,11 +649,11 @@ def add_rollerY_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerY', nodes, axes) + return self._add_bc_type("rollerY", nodes, axes) - def add_rollerZ_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerZ_bc(self, nodes, axes="global"): """Add a roller free on Z boundary condition type to some nodes in a part. Parameters @@ -667,11 +664,11 @@ def add_rollerZ_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerZ', nodes, axes) + return self._add_bc_type("rollerZ", nodes, axes) - def add_rollerXY_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerXY_bc(self, nodes, axes="global"): """Add a roller free on XY boundary condition type to some nodes in a part. Parameters @@ -682,11 +679,11 @@ def add_rollerXY_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerXY', nodes, axes) + return self._add_bc_type("rollerXY", nodes, axes) - def add_rollerXZ_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerXZ_bc(self, nodes, axes="global"): """Add a roller free on XZ boundary condition type to some nodes in a part. Parameters @@ -697,11 +694,11 @@ def add_rollerXZ_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerXZ', nodes, axes) + return self._add_bc_type("rollerXZ", nodes, axes) - def add_rollerYZ_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerYZ_bc(self, nodes, axes="global"): """Add a roller free on YZ boundary condition type to some nodes in a part. Parameters @@ -712,8 +709,9 @@ def add_rollerYZ_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerYZ', nodes, axes) + return self._add_bc_type("rollerYZ", nodes, axes) def remove_bcs(self, nodes): """Release a node previously restrained. @@ -726,6 +724,7 @@ def remove_bcs(self, nodes): Returns ------- None + """ if isinstance(nodes, Node): @@ -738,7 +737,6 @@ def remove_bcs(self, nodes): else: print("WARNING: {!r} was not restrained. skipped!".format(node)) - def remove_all_bcs(self): """Removes all the boundary conditions from the Model. @@ -749,6 +747,7 @@ def remove_all_bcs(self): Returns ------- None + """ for _, nodes in self.bcs.items(): self.remove_bcs(nodes) @@ -759,32 +758,31 @@ def remove_all_bcs(self): # ============================================================================== def _add_ics(self, ic, group): - # type: (_InitialCondition, _Group, str) -> list - """Add a :class:`compas_fea2.model._InitialCondition` to the model. + """Add a :class:`compas_fea2.model.InitialCondition` to the model. Parameters ---------- - ic : :class:`compas_fea2.model._InitialCondition` + ic : :class:`compas_fea2.model.InitialCondition` Initial condition object to add to the model. - group : :class:`compas_fea2.model._Group` + group : :class:`compas_fea2.model.Group` Group of Nodes/Elements where the initial condition is assigned. Returns ------- - :class:`compas_fea2.model._InitialCondition` + :class:`compas_fea2.model.InitialCondition` """ group.part.add_group(group) - if not isinstance(ic, _InitialCondition): - raise TypeError('{!r} is not a _InitialCondition.'.format(ic)) + if not isinstance(ic, InitialCondition): + raise TypeError("{!r} is not a InitialCondition.".format(ic)) for member in group.members: - if not isinstance(member, (Node, _Element)): - raise TypeError('{!r} is not a Node or an Element.'.format(member)) + if not isinstance(member, (Node, Element)): + raise TypeError("{!r} is not a Node or an Element.".format(member)) if not member.part: - raise ValueError('{!r} is not registered to any part.'.format(member)) - elif not member.part in self.parts: - raise ValueError('{!r} belongs to a part not registered to this model.'.format(member)) + raise ValueError("{!r} is not registered to any part.".format(member)) + elif member.part not in self.parts: + raise ValueError("{!r} belongs to a part not registered to this model.".format(member)) member._ic = ic self._ics[ic] = group.members @@ -792,55 +790,51 @@ def _add_ics(self, ic, group): return ic - def add_nodes_ics(self, ic, nodes): - # type: (_InitialCondition, Node, str) -> list - """Add a :class:`compas_fea2.model._InitialCondition` to the model. + """Add a :class:`compas_fea2.model.InitialCondition` to the model. Parameters ---------- - ic : :class:`compas_fea2.model._InitialCondition` + ic : :class:`compas_fea2.model.InitialCondition` Initial condition object to add to the model. nodes : list[:class:`compas_fea2.model.Node`] or :class:`compas_fea2.model.NodesGroup` List or Group with the nodes where the initial condition is assigned. Returns ------- - list[:class:`compas_fea2.model._InitialCondition`] + list[:class:`compas_fea2.model.InitialCondition`] """ if not isinstance(nodes, NodesGroup): - raise TypeError('{} is not a group of nodes'.format(nodes)) + raise TypeError("{} is not a group of nodes".format(nodes)) self._add_ics(ic, nodes) return ic def add_elements_ics(self, ic, elements): - # type: (_InitialCondition, Node, str) -> list - """Add a :class:`compas_fea2.model._InitialCondition` to the model. + """Add a :class:`compas_fea2.model.InitialCondition` to the model. Parameters ---------- - ic : :class:`compas_fea2.model._InitialCondition` + ic : :class:`compas_fea2.model.InitialCondition` Initial condition object to add to the model. elements : :class:`compas_fea2.model.ElementsGroup` List or Group with the elements where the initial condition is assigned. Returns ------- - :class:`compas_fea2.model._InitialCondition` + :class:`compas_fea2.model.InitialCondition` """ if not isinstance(elements, ElementsGroup): - raise TypeError('{} is not a group of elements'.format(elements)) + raise TypeError("{} is not a group of elements".format(elements)) self._add_ics(ic, elements) return ic # ============================================================================== # Summary # ============================================================================== - # TODO add shor/ long + def summary(self): - # type: () -> str """Prints a summary of the Model object. Parameters @@ -851,25 +845,41 @@ def summary(self): ------- str Model summary - """ - parts_info = ['\n'.join(['{}'.format(part.name), - ' # of nodes: {}'.format(len(part.nodes)), - ' # of elements: {}'.format(len(part.elements)), - ' is_rigid : {}'.format('True' if isinstance(part, RigidPart) else 'False')]) for part in self.parts] - constraints_info = '\n'.join([e.__repr__() for e in self.constraints]) + """ + parts_info = [ + "\n".join( + [ + "{}".format(part.name), + " # of nodes: {}".format(len(part.nodes)), + " # of elements: {}".format(len(part.elements)), + " is_rigid : {}".format("True" if isinstance(part, RigidPart) else "False"), + ] + ) + for part in self.parts + ] + + constraints_info = "\n".join([e.__repr__() for e in self.constraints]) bc_info = [] for bc, nodes in self.bcs.items(): for part, part_nodes in groupby(nodes, lambda n: n.part): - bc_info.append('{}: \n{}'.format(part.name, '\n'.join([' {!r} - # of restrained nodes {}'.format(bc, len(list(part_nodes)))]))) - bc_info = '\n'.join(bc_info) + bc_info.append( + "{}: \n{}".format( + part.name, "\n".join([" {!r} - # of restrained nodes {}".format(bc, len(list(part_nodes)))]) + ) + ) + bc_info = "\n".join(bc_info) ic_info = [] for ic, nodes in self.ics.items(): for part, part_nodes in groupby(nodes, lambda n: n.part): - ic_info.append('{}: \n{}'.format(part.name, '\n'.join([' {!r} - # of restrained nodes {}'.format(ic, len(list(part_nodes)))]))) - ic_info = '\n'.join(ic_info) + ic_info.append( + "{}: \n{}".format( + part.name, "\n".join([" {!r} - # of restrained nodes {}".format(ic, len(list(part_nodes)))]) + ) + ) + ic_info = "\n".join(ic_info) data = """ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -894,14 +904,15 @@ def summary(self): Initial Conditions ------------------ {} -""".format(self.name, - self.description or 'N/A', - self.author or 'N/A', - '\n'.join(parts_info), - constraints_info or 'N/A', - bc_info or 'N/A', - ic_info or 'N/A' - ) +""".format( + self.name, + self.description or "N/A", + self.author or "N/A", + "\n".join(parts_info), + constraints_info or "N/A", + bc_info or "N/A", + ic_info or "N/A", + ) print(data) return data @@ -909,32 +920,31 @@ def summary(self): # Save model file # ============================================================================== - def check(self, type='quick'): + def check(self, type="quick"): """Check for possible problems in the model - Warning - ------- - WIP! It is better if you check yourself... + Parameters + ---------- + type : str, optional + *quick* or *deep* check, by default 'quick' + + Returns + ------- + str + report - Parameters - ---------- - type : str, optional - *quick* or *deep* check, by default 'quick' + Warnings + -------- + WIP! It is better if you check yourself... - Returns - ------- - str - report - """ + """ def _check_units(self): - """Check if the units are consistent. - """ + """Check if the units are consistent.""" raise NotImplementedError def _check_bcs(self): - """Check if the units are consistent. - """ + """Check if the units are consistent.""" raise NotImplementedError raise NotImplementedError @@ -960,9 +970,10 @@ def add_problem(self, problem): ------ TypeError if problem is not type :class:`compas_fea2.problem.Problem` + """ if not isinstance(problem, compas_fea2.problem.Problem): - raise TypeError('{} is not a Problem'.format(problem)) + raise TypeError("{} is not a Problem".format(problem)) self._problems.add(problem) problem._registration = self return problem @@ -984,6 +995,7 @@ def add_problems(self, problems): ------ TypeError if a problem is not type :class:`compas_fea2.problem.Problem` + """ return [self.add_problem(problem) for problem in problems] @@ -999,6 +1011,7 @@ def find_problem_by_name(self, name): ------- :class:`compas_fea2.problem.Problem` The problem + """ for problem in self.problems: if problem.name == name: @@ -1009,84 +1022,96 @@ def find_problem_by_name(self, name): # ========================================================================= # @get_docstring(Problem) - @problem_method + # # @problem_method def write_input_file(self, problems=None, path=None, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def analyse(self, problems=None, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def analyze(self, problems=None, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def restart_analysis(self, problem, start, steps, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def analyse_and_extract(self, problems=None, path=None, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def analyse_and_store(self, problems=None, memory_only=False, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def store_results_in_model(self, problems=None, *args, **kwargs): pass # ============================================================================== # Results methods # ============================================================================== - #@get_docstring(Problem) - @problem_method + + # @get_docstring(Problem) + # # @problem_method def get_reaction_forces_sql(self, *, problem=None, step=None): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def get_reaction_moments_sql(self, problem, step=None): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # @problem_method def get_displacements_sql(self, problem, step=None): pass - #@get_docstring(Problem) - @problem_method - def get_max_displacement_sql(self, problem, step=None, component='magnitude'): + # @get_docstring(Problem) + # @problem_method + def get_max_displacement_sql(self, problem, step=None, component="magnitude"): pass - #@get_docstring(Problem) - @problem_method - def get_min_displacement_sql(self, problem, step=None, component='magnitude'): + # @get_docstring(Problem) + # @problem_method + def get_min_displacement_sql(self, problem, step=None, component="magnitude"): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # @problem_method def get_displacement_at_nodes_sql(self, problem, nodes, steps=None): pass - @problem_method + # @problem_method def get_displacement_at_point_sql(self, problem, point, steps=None): pass # ============================================================================== # Viewer # ============================================================================== - def show(self, width=1600, height=900, scale_factor=1., parts=None, - solid=True, draw_nodes=False, node_labels=False, - draw_bcs=1., draw_constraints=True, **kwargs): - """WIP + + def show( + self, + width=1600, + height=900, + scale_factor=1.0, + parts=None, + solid=True, + draw_nodes=False, + node_labels=False, + draw_bcs=1.0, + draw_constraints=True, + **kwargs, + ): + """Visualise the model in the viewer. Parameters ---------- @@ -1108,19 +1133,16 @@ def show(self, width=1600, height=900, scale_factor=1., parts=None, _description_, by default 1. draw_constraints : bool, optional _description_, by default True + """ from compas_fea2.UI.viewer import FEA2Viewer - from compas.geometry import Point, Vector parts = parts or self.parts v = FEA2Viewer(width, height, scale_factor=scale_factor) - v.draw_parts(parts, - draw_nodes, - node_labels, - solid) + v.draw_parts(parts, draw_nodes, node_labels, solid) if draw_bcs: v.draw_bcs(self, parts, draw_bcs) @@ -1130,6 +1152,6 @@ def show(self, width=1600, height=900, scale_factor=1., parts=None, v.show() - @problem_method + # @problem_method def show_displacements(self, problem, *args, **kwargs): pass diff --git a/src/compas_fea2/model/nodes.py b/src/compas_fea2/model/nodes.py index 1a01108ce..aa307467e 100644 --- a/src/compas_fea2/model/nodes.py +++ b/src/compas_fea2/model/nodes.py @@ -6,17 +6,11 @@ from compas.geometry import Point from compas_fea2.base import FEAData -from .bcs import _BoundaryCondition import compas_fea2 -class Node(FEAData): - """Initialises base Node object. - Note - ---- - Nodes are registered to a :class:`compas_fea2.model.DeformablePart` object and can - belong to only one Part. Every time a node is added to a Part, it gets - registered to that Part. +class Node(FEAData): + """Class representing a Node object. Parameters ---------- @@ -69,6 +63,12 @@ class Node(FEAData): temperature : float The temperature at the Node. + Notes + ----- + Nodes are registered to a :class:`compas_fea2.model.DeformablePart` object and can + belong to only one Part. Every time a node is added to a Part, it gets + registered to that Part. + Examples -------- >>> node = Node(xyz=(1.0, 2.0, 3.0)) @@ -85,9 +85,9 @@ def __init__(self, xyz, mass=None, temperature=None, name=None, **kwargs): self._z = xyz[2] self._bc = None - self._dof = {'x': True, 'y': True, 'z': True, 'xx': True, 'yy': True, 'zz': True} + self._dof = {"x": True, "y": True, "z": True, "xx": True, "yy": True, "zz": True} - self._mass = mass if isinstance(mass, tuple) else tuple([mass]*3) + self._mass = mass if isinstance(mass, tuple) else tuple([mass] * 3) self._temperature = temperature self._on_boundary = None @@ -115,8 +115,8 @@ def xyz(self): @xyz.setter def xyz(self, value): - if len(value)!=3: - raise ValueError('Provide a 3 element touple or list') + if len(value) != 3: + raise ValueError("Provide a 3 element touple or list") self._x = value[0] self._y = value[1] self._z = value[2] @@ -151,7 +151,7 @@ def mass(self): @mass.setter def mass(self, value): - self._mass = value if isinstance(value, tuple) else tuple([value]*3) + self._mass = value if isinstance(value, tuple) else tuple([value] * 3) @property def temperature(self): @@ -168,7 +168,7 @@ def gkey(self): @property def dof(self): if self.bc: - return {attr: not bool(getattr(self.bc, attr)) for attr in ['x', 'y', 'z', 'xx', 'yy', 'zz']} + return {attr: not bool(getattr(self.bc, attr)) for attr in ["x", "y", "z", "xx", "yy", "zz"]} else: return self._dof diff --git a/src/compas_fea2/model/parts.py b/src/compas_fea2/model/parts.py index f67b00ccb..5f7b0b2bb 100644 --- a/src/compas_fea2/model/parts.py +++ b/src/compas_fea2/model/parts.py @@ -3,35 +3,38 @@ from __future__ import print_function from math import sqrt -from compas.geometry import Point, Plane, Frame, Polygon +from compas.geometry import Point, Plane, Frame from compas.geometry import Transformation, Scale -from compas.geometry import normalize_vector from compas.geometry import distance_point_point_sqrd from compas.geometry import is_point_in_polygon_xy from compas.geometry import is_point_on_plane from compas.utilities import geometric_key from compas.geometry import Vector -from compas.geometry import sum_vectors import compas_fea2 from compas_fea2.base import FEAData from .nodes import Node -from .elements import _Element, _Element2D, _Element1D, _Element3D, BeamElement, HexahedronElement, ShellElement, TetrahedronElement, Face -from .materials import _Material -from .sections import _Section, ShellSection, SolidSection -from .releases import _BeamEndRelease, BeamEndPinRelease +from .elements import ( + Element, + Element2D, + Element1D, + Element3D, + BeamElement, + HexahedronElement, + ShellElement, + TetrahedronElement, +) +from .materials import Material +from .sections import Section, ShellSection, SolidSection +from .releases import BeamEndRelease from .groups import NodesGroup, ElementsGroup, FacesGroup -from .ics import InitialStressField from compas_fea2.utilities._utils import timer -class _Part(FEAData): - """ - Note - ---- - Parts are registered to a :class:`compas_fea2.model.Model`. +class Part(FEAData): + """Base class for Parts. Parameters ---------- @@ -52,13 +55,13 @@ class _Part(FEAData): Number of nodes in the part. gkey_node : {gkey : :class:`compas_fea2.model.Node`} Dictionary that associates each node and its geometric key} - materials : Set[:class:`compas_fea2.model._Material`] + materials : Set[:class:`compas_fea2.model.Material`] The materials belonging to the part. - sections : Set[:class:`compas_fea2.model._Section`] + sections : Set[:class:`compas_fea2.model.Section`] The sections belonging to the part. - elements : Set[:class:`compas_fea2.model._Element`] + elements : Set[:class:`compas_fea2.model.Element`] The elements belonging to the part. - element_types : {:class:`compas_fea2.model._Element` : [:class:`compas_fea2.model._Element`]] + element_types : {:class:`compas_fea2.model.Element` : [:class:`compas_fea2.model.Element`]] Dictionary with the elements of the part for each element type. element_count : int Number of elements in the part @@ -72,10 +75,15 @@ class _Part(FEAData): The outer boundary mesh enveloping the Part. discretized_boundary_mesh : :class:`compas.datastructures.Mesh` The discretized outer boundary mesh enveloping the Part. + + Notes + ----- + Parts are registered to a :class:`compas_fea2.model.Model`. + """ def __init__(self, name=None, **kwargs): - super(_Part, self).__init__(name=name, **kwargs) + super(Part, self).__init__(name=name, **kwargs) self._nodes = set() self._gkey_node = {} self._sections = set() @@ -137,7 +145,7 @@ def discretized_boundary_mesh(self): @property def volume(self): - self._volume = 0. + self._volume = 0.0 for element in self.elements: if element.volume: self._volume += element.volume @@ -153,40 +161,39 @@ def results(self): @property def nodes_count(self): - return len(self.nodes)-1 + return len(self.nodes) - 1 @property def elements_count(self): - return len(self.elements)-1 + return len(self.elements) - 1 @property def element_types(self): element_types = {} for element in self.elements: - element_types.setdefault(type(element),[]).append(element) + element_types.setdefault(type(element), []).append(element) return element_types + # def __str__(self): + # return """ + # {} + # {} + # name : {} -# def __str__(self): -# return """ -# {} -# {} -# name : {} - -# number of elements : {} -# number of nodes : {} -# """.format(self.__class__.__name__, -# len(self.__class__.__name__) * '-', -# self.name, -# self.elements_count, -# self.nodes_count) + # number of elements : {} + # number of nodes : {} + # """.format(self.__class__.__name__, + # len(self.__class__.__name__) * '-', + # self.name, + # self.elements_count, + # self.nodes_count) # ========================================================================= # Constructor methods # ========================================================================= @classmethod - def from_compas_line(cls, line, element_model='BeamElement', section=None, name=None, **kwargs): + def from_compas_line(cls, line, element_model="BeamElement", section=None, name=None, **kwargs): """Generate a part from a class:`compas.geometry.Line`. Parameters @@ -204,19 +211,22 @@ def from_compas_line(cls, line, element_model='BeamElement', section=None, name= ------- class:`compas_fea2.model.Part` The part. + """ import compas_fea2 + prt = cls(name=name) element = getattr(compas_fea2.model, element_model)(nodes=[Node(line.start), Node(line.end)], section=section) - if not isinstance(element, _Element1D): + if not isinstance(element, Element1D): raise ValueError("Provide a 1D element") prt.add_element(element) return prt @classmethod - @timer(message='compas Mesh successfully imported in ') + @timer(message="compas Mesh successfully imported in ") def shell_from_compas_mesh(cls, mesh, section, name=None, **kwargs): """Creates a DeformablePart object from a :class:`compas.datastructures.Mesh`. + To each face of the mesh is assigned a :class:`compas_fea2.model.ShellElement` objects. Currently, the same section is applied to all the elements. @@ -231,7 +241,7 @@ def shell_from_compas_mesh(cls, mesh, section, name=None, **kwargs): automatically. """ - implementation = kwargs.get('implementation', None) + implementation = kwargs.get("implementation", None) part = cls(name, **kwargs) vertex_node = {vertex: part.add_node(Node(mesh.vertex_coordinates(vertex))) for vertex in mesh.vertices()} @@ -249,19 +259,11 @@ def shell_from_compas_mesh(cls, mesh, section, name=None, **kwargs): @classmethod # @timer(message='part successfully imported from gmsh model in ') def from_gmsh(cls, gmshModel, name=None, **kwargs): - """Create a Part object from a gmshModel object. According to the - `section` type provided, :class:`compas_fea2.model._Element2D` or - :class:`compas_fea2.model._Element3D` elements are cretated. - The same section is applied to all the elements. - - Note - ---- - The gmshModel must have the right dimension corresponding to the section - provided. + """Create a Part object from a gmshModel object. - Warning - ------- - the `split` option is currently not implemented + According to the `section` type provided, :class:`compas_fea2.model.Element2D` or + :class:`compas_fea2.model.Element3D` elements are cretated. + The same section is applied to all the elements. Parameters ---------- @@ -284,9 +286,13 @@ def from_gmsh(cls, gmshModel, name=None, **kwargs): Returns ------- - :class:`compas_fea2.model._Part` + :class:`compas_fea2.model.Part` The part meshed. + Notes + ----- + The gmshModel must have the right dimension corresponding to the section provided. + References ---------- .. [1] https://gitlab.onelab.info/gmsh/gmsh/blob/gmsh_4_9_1/api/gmsh.py @@ -304,47 +310,44 @@ def from_gmsh(cls, gmshModel, name=None, **kwargs): part = cls(name=name) # add nodes gmsh_nodes = gmshModel.mesh.get_nodes() - node_coords = gmsh_nodes[1].reshape((-1, 3), order='C') + node_coords = gmsh_nodes[1].reshape((-1, 3), order="C") fea2_nodes = [part.add_node(Node(coords.tolist())) for coords in node_coords] # add elements gmsh_elements = gmshModel.mesh.get_elements() - section = kwargs.get('section', None) - split = kwargs.get('split', False) - verbose = kwargs.get('verbose', False) - rigid = kwargs.get('rigid', False) - implementation = kwargs.get('implementation', None) + section = kwargs.get("section", None) + split = kwargs.get("split", False) + verbose = kwargs.get("verbose", False) + rigid = kwargs.get("rigid", False) + implementation = kwargs.get("implementation", None) dimension = 2 if isinstance(section, SolidSection) else 1 - ntags_per_element = np.split(gmsh_elements[2][dimension]-1, - len(gmsh_elements[1][dimension])) # gmsh keys start from 1 + ntags_per_element = np.split( + gmsh_elements[2][dimension] - 1, len(gmsh_elements[1][dimension]) + ) # gmsh keys start from 1 for ntags in ntags_per_element: if split: - raise NotImplementedError('this feature is under development') + raise NotImplementedError("this feature is under development") element_nodes = [fea2_nodes[ntag] for ntag in ntags] if ntags.size == 3: - k = part.add_element(ShellElement(nodes=element_nodes, - section=section, - rigid=rigid, - implementation=implementation)) + k = part.add_element( + ShellElement(nodes=element_nodes, section=section, rigid=rigid, implementation=implementation) + ) elif ntags.size == 4: if isinstance(section, ShellSection): - k = part.add_element(ShellElement(nodes=element_nodes, - section=section, - rigid=rigid, - implementation=implementation)) + k = part.add_element( + ShellElement(nodes=element_nodes, section=section, rigid=rigid, implementation=implementation) + ) else: - k = part.add_element(TetrahedronElement(nodes=element_nodes, - section=section)) + k = part.add_element(TetrahedronElement(nodes=element_nodes, section=section)) elif ntags.size == 8: - k = part.add_element(HexahedronElement(nodes=element_nodes, - section=section)) + k = part.add_element(HexahedronElement(nodes=element_nodes, section=section)) else: - raise NotImplementedError('Element with {} nodes not supported'.format(ntags.size)) + raise NotImplementedError("Element with {} nodes not supported".format(ntags.size)) if verbose: - print('element {} added'.format(k)) + print("element {} added".format(k)) return part @@ -369,11 +372,12 @@ def from_boundary_mesh(cls, boundary_mesh, name=None, **kwargs): """ from compas_gmsh.models import MeshModel - target_mesh_size = kwargs.get('target_mesh_size', 1) - mesh_size_at_vertices = kwargs.get('mesh_size_at_vertices', None) - target_point_mesh_size = kwargs.get('target_point_mesh_size', None) - meshsize_max = kwargs.get('meshsize_max', None) - meshsize_min = kwargs.get('meshsize_min', None) + + target_mesh_size = kwargs.get("target_mesh_size", 1) + mesh_size_at_vertices = kwargs.get("mesh_size_at_vertices", None) + target_point_mesh_size = kwargs.get("target_point_mesh_size", None) + meshsize_max = kwargs.get("meshsize_max", None) + meshsize_min = kwargs.get("meshsize_min", None) gmshModel = MeshModel.from_mesh(boundary_mesh, targetlength=target_mesh_size) @@ -401,9 +405,9 @@ def from_boundary_mesh(cls, boundary_mesh, name=None, **kwargs): gmshModel.generate_mesh(2) part._discretized_boundary_mesh = gmshModel.mesh_to_compas() - del(gmshModel) + del gmshModel - if kwargs.get('rigid', False): + if kwargs.get("rigid", False): point = boundary_mesh.centroid() part.reference_point = Node(xyz=[point.x, point.y, point.z]) @@ -417,6 +421,7 @@ def from_boundary_mesh(cls, boundary_mesh, name=None, **kwargs): # ========================================================================= # Nodes methods # ========================================================================= + def find_node_by_key(self, key): # type: (int) -> Node """Retrieve a node in the model using its key. @@ -430,6 +435,7 @@ def find_node_by_key(self, key): ------- :class:`compas_fea2.model.Node` The corresponding node. + """ for node in self.nodes: if node.key == key: @@ -471,12 +477,11 @@ def find_nodes_by_location(self, point, distance, plane=None, report=False, **kw list[:class:`compas_fea2.model.Node`] """ - d2 = distance ** 2 + d2 = distance**2 nodes = self.find_nodes_on_plane(plane) if plane else self.nodes if report: - return {node: sqrt(distance) for node in nodes if (distance := distance_point_point_sqrd(node.xyz, point)) < d2} - else: - return [node for node in nodes if (distance := distance_point_point_sqrd(node.xyz, point)) < d2] + return {node: sqrt(distance) for node in nodes if distance_point_point_sqrd(node.xyz, point) < d2} + return [node for node in nodes if distance_point_point_sqrd(node.xyz, point) < d2] def find_closest_nodes_to_point(self, point, distance, number_of_nodes=1, plane=None): # type: (Point, float, int, Plane) -> list(Node) @@ -520,6 +525,7 @@ def find_nodes_around_node(self, node, distance, plane=None): ------- [:class:`compas_fea2.model.Node] The nodes around the given node + """ nodes = self.find_nodes_by_location(node.xyz, distance, plane, report=True) if node in nodes: @@ -555,10 +561,6 @@ def find_nodes_by_attribute(self, attr, value, tolerance=0.001): # type: (str, float, float) -> list(Node) """Find all nodes with a given value for a the given attribute. - Note - ---- - Only numeric attributes are supported. - Parameters ---------- attr : str @@ -570,6 +572,10 @@ def find_nodes_by_attribute(self, attr, value, tolerance=0.001): ------- list[:class:`compas_fea2.model.Node`] + Notes + ----- + Only numeric attributes are supported. + """ return list(filter(lambda x: abs(getattr(x, attr) - value) <= tolerance, self.nodes)) @@ -590,7 +596,6 @@ def find_nodes_on_plane(self, plane): return list(filter(lambda x: is_point_on_plane(Point(*x.xyz), plane), self.nodes)) def find_nodes_in_polygon(self, polygon, tolerance=1.1): - # type: (Polygon, float) -> list(Node) """Find the nodes of the part that are contained within a planar polygon Parameters @@ -602,22 +607,22 @@ def find_nodes_in_polygon(self, polygon, tolerance=1.1): ------- [:class:`compas_fea2.model.Node] List with the nodes contained in the polygon. + """ # TODO quick fix...change! - if not hasattr(polygon, 'plane'): + if not hasattr(polygon, "plane"): try: polygon.plane = Frame.from_points(*polygon.points[:3]) - except: + except Exception: polygon.plane = Frame.from_points(*polygon.points[-3:]) - S = Scale.from_factors([tolerance]*3, polygon.plane) + S = Scale.from_factors([tolerance] * 3, polygon.plane) T = Transformation.from_frame_to_frame(polygon.plane, Frame.worldXY()) nodes_on_plane = self.find_nodes_on_plane(Plane.from_frame(polygon.plane)) polygon_xy = polygon.transformed(S) polygon_xy = polygon.transformed(T) return list(filter(lambda x: is_point_in_polygon_xy(Point(*x.xyz).transformed(T), polygon_xy), nodes_on_plane)) - # TODO quite slow...check how to make it faster def find_nodes_where(self, conditions): # type: (list(str)) -> list(Node) @@ -632,8 +637,10 @@ def find_nodes_where(self, conditions): ------- [Node] List with the nodes matching the criteria. + """ import re + nodes = [] for condition in conditions: # limit the serch to the already found nodes @@ -642,7 +649,9 @@ def find_nodes_where(self, conditions): eval(condition) except NameError as ne: var_name = re.findall(r"'([^']*)'", str(ne))[0] - nodes.append(set(filter(lambda n: eval(condition.replace(var_name, str(getattr(n, var_name)))), part_nodes))) + nodes.append( + set(filter(lambda n: eval(condition.replace(var_name, str(getattr(n, var_name)))), part_nodes)) + ) return list(set.intersection(*nodes)) def contains_node(self, node): @@ -664,17 +673,13 @@ def add_node(self, node): # type: (Node) -> Node """Add a node to the part. - Note - ---- - By adding a Node to the part, it gets registered to the part. - Parameters ---------- node : :class:`compas_fea2.model.Node` The node. - Return - ------ + Returns + ------- :class:`compas_fea2.model.Node` The identifier of the node in the part. @@ -683,6 +688,10 @@ def add_node(self, node): TypeError If the node is not a node. + Notes + ----- + By adding a Node to the part, it gets registered to the part. + Examples -------- >>> part = DeformablePart() @@ -691,17 +700,17 @@ def add_node(self, node): """ if not isinstance(node, Node): - raise TypeError('{!r} is not a node.'.format(node)) + raise TypeError("{!r} is not a node.".format(node)) if self.contains_node(node): if compas_fea2.VERBOSE: - print('NODE SKIPPED: Node {!r} already in part.'.format(node)) + print("NODE SKIPPED: Node {!r} already in part.".format(node)) return if not compas_fea2.POINT_OVERLAP: if self.find_nodes_by_location(node.xyz, distance=compas_fea2.GLOBAL_TOLERANCE): if compas_fea2.VERBOSE: - print('NODE SKIPPED: Part {!r} has already a node at {}.'.format(self, node.xyz)) + print("NODE SKIPPED: Part {!r} has already a node at {}.".format(self, node.xyz)) return node._key = len(self._nodes) @@ -709,7 +718,7 @@ def add_node(self, node): self._gkey_node[node.gkey] = node node._registration = self if compas_fea2.VERBOSE: - print('Node {!r} registered to {!r}.'.format(node, self)) + print("Node {!r} registered to {!r}.".format(node, self)) return node def add_nodes(self, nodes): @@ -721,8 +730,8 @@ def add_nodes(self, nodes): nodes : list[:class:`compas_fea2.model.Node`] The list of nodes. - Return - ------ + Returns + ------- list[:class:`compas_fea2.model.Node`] The identifiers of the nodes in the part. @@ -740,14 +749,15 @@ def add_nodes(self, nodes): def remove_node(self, node): """Remove a :class:`compas_fea2.model.Node` from the part. - Warning - ------- + Warnings + -------- Removing nodes can cause inconsistencies. Parameters ---------- node : :class:`compas_fea2.model.Node` The node to remove + """ # type: (Node) -> None if self.contains_node(node): @@ -755,19 +765,20 @@ def remove_node(self, node): self._gkey_node.pop(node.gkey) node._registration = None if compas_fea2.VERBOSE: - print('Node {!r} removed from {!r}.'.format(node, self)) + print("Node {!r} removed from {!r}.".format(node, self)) def remove_nodes(self, nodes): """Remove multiple :class:`compas_fea2.model.Node` from the part. - Warning - ------- + Warnings + -------- Removing nodes can cause inconsistencies. Parameters ---------- - nodes : []:class:`compas_fea2.model.Node`] + nodes : [:class:`compas_fea2.model.Node`] List with the nodes to remove + """ for node in nodes: self.remove_node(node) @@ -783,28 +794,29 @@ def is_node_on_boundary(self, node, precision=None): precision : ?? ??? - Note - ---- - The `discretized_boundary_mesh` of the part must have been previously - defined. - Returns ------- bool `True` if the node is on the boundary, `False` otherwise. + + Notes + ----- + The `discretized_boundary_mesh` of the part must have been previously defined. + """ if not self.discretized_boundary_mesh: raise AttributeError("The discretized_boundary_mesh has not been defined") if not node.on_boundary: - node._on_boundary = True if geometric_key( - node.xyz, precision) in self.discretized_boundary_mesh.gkey_vertex() else False + node._on_boundary = ( + True if geometric_key(node.xyz, precision) in self.discretized_boundary_mesh.gkey_vertex() else False + ) return node.on_boundary # ========================================================================= # Elements methods # ========================================================================= def find_element_by_key(self, key): - # type: (int) -> _Element + # type: (int) -> Element """Retrieve an element in the model using its key. Parameters @@ -814,15 +826,16 @@ def find_element_by_key(self, key): Returns ------- - :class:`compas_fea2.model._Element` + :class:`compas_fea2.model.Element` The corresponding element. + """ for element in self.elements: if element.key == key: return element def find_elements_by_name(self, name): - # type: (str) -> list(_Element) + # type: (str) -> list(Element) """Find all elements with a given name. Parameters @@ -831,18 +844,18 @@ def find_elements_by_name(self, name): Returns ------- - list[:class:`compas_fea2.model._Element`] + list[:class:`compas_fea2.model.Element`] """ return [element for element in self.elements if element.name == name] def contains_element(self, element): - # type: (_Element) -> _Element + # type: (Element) -> Element """Verify that the part contains a specific element. Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` Returns ------- @@ -852,17 +865,17 @@ def contains_element(self, element): return element in self.elements def add_element(self, element): - # type: (_Element) -> _Element + # type: (Element) -> Element """Add an element to the part. Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element instance. Returns ------- - :class:`compas_fea2.model._Element` + :class:`compas_fea2.model.Element` Raises ------ @@ -870,8 +883,8 @@ def add_element(self, element): If the element is not an element. """ - if not isinstance(element, _Element): - raise TypeError('{!r} is not an element.'.format(element)) + if not isinstance(element, Element): + raise TypeError("{!r} is not an element.".format(element)) if self.contains_element(element): if compas_fea2.VERBOSE: @@ -879,11 +892,11 @@ def add_element(self, element): return self.add_nodes(element.nodes) - if hasattr(element, 'section'): + if hasattr(element, "section"): if element.section: self.add_section(element.section) - if hasattr(element.section, 'material'): + if hasattr(element.section, "material"): if element.section.material: self.add_material(element.section.material) @@ -891,54 +904,56 @@ def add_element(self, element): self.elements.add(element) element._registration = self if compas_fea2.VERBOSE: - print('Element {!r} registered to {!r}.'.format(element, self)) + print("Element {!r} registered to {!r}.".format(element, self)) return element def add_elements(self, elements): - # type: (_Element) -> list(_Element) + # type: (Element) -> list(Element) """Add multiple elements to the part. Parameters ---------- - elements : list[:class:`compas_fea2.model._Element`] + elements : list[:class:`compas_fea2.model.Element`] - Return - ------ - list[:class:`compas_fea2.model._Element`] + Returns + ------- + list[:class:`compas_fea2.model.Element`] """ return [self.add_element(element) for element in elements] def remove_element(self, element): - """Remove a :class:`compas_fea2.model._Element` from the part. - - Warning - ------- - Removing elements can cause inconsistencies. + """Remove a :class:`compas_fea2.model.Element` from the part. Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element to remove + + Warnings + -------- + Removing elements can cause inconsistencies. + """ - # type: (_Element) -> None + # type: (Element) -> None if self.contains_node(element): self.elements.pop(element) element._registration = None if compas_fea2.VERBOSE: - print('Element {!r} removed from {!r}.'.format(element, self)) + print("Element {!r} removed from {!r}.".format(element, self)) def remove_elements(self, elements): - """Remove multiple :class:`compas_fea2.model._Element` from the part. - - Warning - ------- - Removing elements can cause inconsistencies. + """Remove multiple :class:`compas_fea2.model.Element` from the part. Parameters ---------- - elements : []:class:`compas_fea2.model._Element`] + elements : []:class:`compas_fea2.model.Element`] List with the elements to remove + + Warnings + -------- + Removing elements can cause inconsistencies. + """ for element in elements: self.remove_element(element) @@ -948,15 +963,16 @@ def is_element_on_boundary(self, element): Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element to check. Returns ------- bool ``True`` if the element is on the boundary. + """ - # type: (_Element) -> bool + # type: (Element) -> bool from compas.geometry import centroid_points if element.on_boundary is None: @@ -964,13 +980,20 @@ def is_element_on_boundary(self, element): centroid_face = {} for face in self._discretized_boundary_mesh.faces(): centroid_face[geometric_key(self._discretized_boundary_mesh.face_centroid(face))] = face - if isinstance(element, _Element3D): - if any(geometric_key(centroid_points([node.xyz for node in face.nodes])) in self._discretized_boundary_mesh.centroid_face for face in element.faces): + if isinstance(element, Element3D): + if any( + geometric_key(centroid_points([node.xyz for node in face.nodes])) + in self._discretized_boundary_mesh.centroid_face + for face in element.faces + ): element.on_boundary = True else: element.on_boundary = False - elif isinstance(element, _Element2D): - if geometric_key(centroid_points([node.xyz for node in element.nodes])) in self._discretized_boundary_mesh.centroid_face: + elif isinstance(element, Element2D): + if ( + geometric_key(centroid_points([node.xyz for node in element.nodes])) + in self._discretized_boundary_mesh.centroid_face + ): element.on_boundary = True else: element.on_boundary = False @@ -981,13 +1004,8 @@ def is_element_on_boundary(self, element): # ========================================================================= def find_faces_on_plane(self, plane): - # type: (Plane) -> list(Face) """Find the face of the elements that belongs to a given plane, if any. - Note - ---- - The search is limited to solid elements. - Parameters ---------- plane : :class:`compas.geometry.Plane` @@ -997,9 +1015,16 @@ def find_faces_on_plane(self, plane): ------- [:class:`compas_fea2.model.Face`] list with the faces belonging to the given plane. + + Notes + ----- + The search is limited to solid elements. + """ faces = [] - for element in filter(lambda x: isinstance(x, (_Element2D, _Element3D)) and self.is_element_on_boundary(x), self._elements): + for element in filter( + lambda x: isinstance(x, (Element2D, Element3D)) and self.is_element_on_boundary(x), self._elements + ): for face in element.faces: if all([is_point_on_plane(node.xyz, plane) for node in face.nodes]): faces.append(face) @@ -1042,7 +1067,7 @@ def contains_group(self, group): elif isinstance(group, FacesGroup): return group in self._facesgroups else: - raise TypeError('{!r} is not a valid Group'.format(group)) + raise TypeError("{!r} is not a valid Group".format(group)) def add_group(self, group): """Add a node or element group to the part. @@ -1051,8 +1076,8 @@ def add_group(self, group): ---------- group : :class:`compas_fea2.model.NodeGroup` | :class:`compas_fea2.model.ElementGroup` - Return - ------ + Returns + ------- None Raises @@ -1089,8 +1114,8 @@ def add_groups(self, groups): ---------- groups : list[:class:`compas_fea2.model.Group`] - Return - ------ + Returns + ------- list[:class:`compas_fea2.model.Group`] """ @@ -1100,14 +1125,14 @@ def add_groups(self, groups): # Results methods # ============================================================================== - def sorted_nodes_by_displacement(self, problem, step=None, component='length'): + def sorted_nodes_by_displacement(self, problem, step=None, component="length"): """Return a list with the nodes sorted by their displacement Parameters ---------- problem : :class:`compas_fea2.problem.Problem` The problem - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The step, by default None. If not provided, the last step of the problem is used. component : str, optional @@ -1117,18 +1142,19 @@ def sorted_nodes_by_displacement(self, problem, step=None, component='length'): ------- [:class:`compas_fea2.model.Node`] The node sorted by displacment (ascending). + """ step = step or problem._steps_order[-1] - return sorted(self.nodes, key=lambda n: getattr(Vector(*n.results[problem][step].get('U', None)), component)) + return sorted(self.nodes, key=lambda n: getattr(Vector(*n.results[problem][step].get("U", None)), component)) - def get_max_displacement(self, problem, step=None, component='length'): + def get_max_displacement(self, problem, step=None, component="length"): """Retrieve the node with the maximum displacement Parameters ---------- problem : :class:`compas_fea2.problem.Problem` The problem - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The step, by default None. If not provided, the last step of the problem is used. component : str, optional @@ -1138,20 +1164,21 @@ def get_max_displacement(self, problem, step=None, component='length'): ------- :class:`compas_fea2.model.Node`, float The node and the displacement + """ step = step or problem._steps_order[-1] node = self.sorted_nodes_by_displacement(problem=problem, step=step, component=component)[-1] - displacement = getattr(Vector(*node.results[problem][step].get('U', None)), component) + displacement = getattr(Vector(*node.results[problem][step].get("U", None)), component) return node, displacement - def get_min_displacement(self, problem, step=None, component='length'): + def get_min_displacement(self, problem, step=None, component="length"): """Retrieve the node with the minimum displacement Parameters ---------- problem : :class:`compas_fea2.problem.Problem` The problem - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The step, by default None. If not provided, the last step of the problem is used. component : str, optional @@ -1161,20 +1188,21 @@ def get_min_displacement(self, problem, step=None, component='length'): ------- :class:`compas_fea2.model.Node`, float The node and the displacement + """ step = step or problem._steps_order[-1] node = self.sorted_nodes_by_displacement(problem=problem, step=step, component=component)[0] - displacement = getattr(Vector(*node.results[problem][step].get('U', None)), component) + displacement = getattr(Vector(*node.results[problem][step].get("U", None)), component) return node, displacement - def get_average_displacement_at_point(self, problem, point, distance, step=None, component='length', project=False): + def get_average_displacement_at_point(self, problem, point, distance, step=None, component="length", project=False): """Compute the average displacement around a point Parameters ---------- problem : :class:`compas_fea2.problem.Problem` The problem - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The step, by default None. If not provided, the last step of the problem is used. component : str, optional @@ -1184,27 +1212,27 @@ def get_average_displacement_at_point(self, problem, point, distance, step=None, ------- :class:`compas_fea2.model.Node`, float The node and the displacement + """ step = step or problem._steps_order[-1] nodes = self.find_nodes_by_location(point=point, distance=distance, report=True) if nodes: - displacements = [getattr(Vector(*node.results[problem][step].get('U', None)), component) for node in nodes] - return point, sum(displacements)/len(displacements) + displacements = [getattr(Vector(*node.results[problem][step].get("U", None)), component) for node in nodes] + return point, sum(displacements) / len(displacements) -class DeformablePart(_Part): +class DeformablePart(Part): """Deformable part. - """ - __doc__ += _Part.__doc__ - __doc__ += """ - Additional Attributes - --------------------- - materials : Set[:class:`compas_fea2.model._Material`] + + Attributes + ---------- + materials : Set[:class:`compas_fea2.model.Material`] The materials belonging to the part. - sections : Set[:class:`compas_fea2.model._Section`] + sections : Set[:class:`compas_fea2.model.Section`] The sections belonging to the part. - releases : Set[:class:`compas_fea2.model._BeamEndRelease`] + releases : Set[:class:`compas_fea2.model.BeamEndRelease`] The releases belonging to the part. + """ def __init__(self, name=None, **kwargs): @@ -1213,15 +1241,15 @@ def __init__(self, name=None, **kwargs): self._sections = set() self._releases = set() - @ property + @property def materials(self): return self._materials - @ property + @property def sections(self): return self._sections - @ property + @property def releases(self): return self._releases @@ -1229,10 +1257,11 @@ def releases(self): # Constructor methods # ========================================================================= - @ classmethod - @ timer(message='compas Mesh successfully imported in ') + @classmethod + @timer(message="compas Mesh successfully imported in ") def frame_from_compas_mesh(cls, mesh, section, name=None, **kwargs): """Creates a DeformablePart object from a a :class:`compas.datastructures.Mesh`. + To each edge of the mesh is assigned a :class:`compas_fea2.model.BeamElement`. Currently, the same section is applied to all the elements. @@ -1251,7 +1280,7 @@ def frame_from_compas_mesh(cls, mesh, section, name=None, **kwargs): for edge in mesh.edges(): nodes = [vertex_node[vertex] for vertex in edge] - v = mesh.edge_direction(*edge) + v = list(mesh.edge_direction(edge)) v.append(v.pop(0)) part.add_element(BeamElement(nodes=[*nodes], section=section, frame=v)) @@ -1260,17 +1289,15 @@ def frame_from_compas_mesh(cls, mesh, section, name=None, **kwargs): return part - @ classmethod + @classmethod # @timer(message='part successfully imported from gmsh model in ') def from_gmsh(cls, gmshModel, section, name=None, **kwargs): - """ - """ + """ """ return super().from_gmsh(gmshModel, name=name, section=section, **kwargs) - @ classmethod + @classmethod def from_boundary_mesh(cls, boundary_mesh, section, name=None, **kwargs): - """ - """ + """ """ return super().from_boundary_mesh(boundary_mesh, section=section, name=name, **kwargs) # ========================================================================= @@ -1278,7 +1305,7 @@ def from_boundary_mesh(cls, boundary_mesh, section, name=None, **kwargs): # ========================================================================= def find_materials_by_name(self, name): - # type: (str) -> list(_Material) + # type: (str) -> list(Material) """Find all materials with a given name. Parameters @@ -1293,7 +1320,7 @@ def find_materials_by_name(self, name): return [material for material in self.materials if material.name == name] def contains_material(self, material): - # type: (_Material) -> _Material + # type: (Material) -> Material """Verify that the part contains a specific material. Parameters @@ -1308,7 +1335,7 @@ def contains_material(self, material): return material in self.materials def add_material(self, material): - # type: (_Material) -> _Material + # type: (Material) -> Material """Add a material to the part so that it can be referenced in section and element definitions. Parameters @@ -1325,12 +1352,12 @@ def add_material(self, material): If the material is not a material. """ - if not isinstance(material, _Material): - raise TypeError('{!r} is not a material.'.format(material)) + if not isinstance(material, Material): + raise TypeError("{!r} is not a material.".format(material)) if self.contains_material(material): if compas_fea2.VERBOSE: - print('SKIPPED: Material {!r} already in part.'.format(material)) + print("SKIPPED: Material {!r} already in part.".format(material)) return material._key = len(self._materials) @@ -1339,7 +1366,7 @@ def add_material(self, material): return material def add_materials(self, materials): - # type: (_Material) -> list(_Material) + # type: (Material) -> list(Material) """Add multiple materials to the part. Parameters @@ -1358,7 +1385,7 @@ def add_materials(self, materials): # ========================================================================= def find_sections_by_name(self, name): - # type: (str) -> list(_Section) + # type: (str) -> list(Section) """Find all sections with a given name. Parameters @@ -1373,7 +1400,7 @@ def find_sections_by_name(self, name): return [section for section in self.sections if section.name == name] def contains_section(self, section): - # type: (_Section) -> _Section + # type: (Section) -> Section """Verify that the part contains a specific section. Parameters @@ -1388,7 +1415,7 @@ def contains_section(self, section): return section in self.sections def add_section(self, section): - # type: (_Section) -> _Section + # type: (Section) -> Section """Add a section to the part so that it can be referenced in element definitions. Parameters @@ -1405,8 +1432,8 @@ def add_section(self, section): If the section is not a section. """ - if not isinstance(section, _Section): - raise TypeError('{!r} is not a section.'.format(section)) + if not isinstance(section, Section): + raise TypeError("{!r} is not a section.".format(section)) if self.contains_section(section): if compas_fea2.VERBOSE: @@ -1420,7 +1447,7 @@ def add_section(self, section): return section def add_sections(self, sections): - # type: (list(_Section)) -> _Section + # type: (list(Section)) -> Section """Add multiple sections to the part. Parameters @@ -1439,8 +1466,7 @@ def add_sections(self, sections): # ========================================================================= def add_beam_release(self, element, location, release): - """Add a :class:`compas_fea2.model._BeamEndRelease` to an element in the - part. + """Add a :class:`compas_fea2.model.BeamEndRelease` to an element in the part. Parameters ---------- @@ -1448,73 +1474,72 @@ def add_beam_release(self, element, location, release): The element to release. location : str 'start' or 'end'. - release : :class:`compas_fea2.model._BeamEndRelease` + release : :class:`compas_fea2.model.BeamEndRelease` Release type to apply. + """ - if not isinstance(release, _BeamEndRelease): - raise TypeError('{!r} is not a beam release element.'.format(release)) + if not isinstance(release, BeamEndRelease): + raise TypeError("{!r} is not a beam release element.".format(release)) release.element = element release.location = location self._releases.add(release) return release -class RigidPart(_Part): +class RigidPart(Part): """Rigid part. - """ - __doc__ += _Part.__doc__ - __doc__ += """ - Addtional Attributes - -------------------- + + Attributes + ---------- reference_point : :class:`compas_fea2.model.Node` A node acting as a reference point for the part, by default `None`. This is required if the part is rigid as it controls its movement in space. + """ def __init__(self, reference_point=None, name=None, **kwargs): super(RigidPart, self).__init__(name=name, **kwargs) self._reference_point = reference_point - @ property + @property def reference_point(self): return self._reference_point - @ reference_point.setter + @reference_point.setter def reference_point(self, value): self._reference_point = self.add_node(value) value._is_reference = True - @ classmethod + @classmethod # @timer(message='part successfully imported from gmsh model in ') def from_gmsh(cls, gmshModel, name=None, **kwargs): - """ - """ - kwargs['rigid'] = True + """ """ + kwargs["rigid"] = True return super().from_gmsh(gmshModel, name=name, **kwargs) - @ classmethod + @classmethod def from_boundary_mesh(cls, boundary_mesh, name=None, **kwargs): - """ - """ - kwargs['rigid'] = True + """ """ + kwargs["rigid"] = True return super().from_boundary_mesh(boundary_mesh, name=name, **kwargs) + # ========================================================================= # Elements methods # ========================================================================= # TODO this can be removed and the checks on the rigid part can be done in _part def add_element(self, element): - # type: (_Element) -> _Element + # type: (Element) -> Element """Add an element to the part. Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element instance. Returns ------- - :class:`compas_fea2.model._Element` + :class:`compas_fea2.model.Element` Raises ------ @@ -1522,8 +1547,8 @@ def add_element(self, element): If the element is not an element. """ - if not hasattr(element, 'rigid'): - raise TypeError('The element type cannot be assigned to a RigidPart') - if not getattr(element, 'rigid'): - raise TypeError('Rigid parts can only have rigid elements') + if not hasattr(element, "rigid"): + raise TypeError("The element type cannot be assigned to a RigidPart") + if not getattr(element, "rigid"): + raise TypeError("Rigid parts can only have rigid elements") return super().add_element(element) diff --git a/src/compas_fea2/model/releases.py b/src/compas_fea2/model/releases.py index 317e2715d..e1b0bfe17 100644 --- a/src/compas_fea2/model/releases.py +++ b/src/compas_fea2/model/releases.py @@ -3,18 +3,14 @@ from __future__ import print_function from compas_fea2.base import FEAData -from compas.geometry import Frame import compas_fea2.model -class _BeamEndRelease(FEAData): +class BeamEndRelease(FEAData): """Assign a general end release to a `compas_fea2.model.BeamElement`. Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. n : bool, optional Release displacements along the local axial direction, by default False v1 : bool, optional @@ -30,9 +26,6 @@ class _BeamEndRelease(FEAData): Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. location : str 'start' or 'end' element : :class:`compas_fea2.model.BeamElement` @@ -52,8 +45,8 @@ class _BeamEndRelease(FEAData): """ - def __init__(self, n=False, v1=False, v2=False, m1=False, m2=False, t=False, name=None, **kwargs): - super(_BeamEndRelease, self).__init__(name, **kwargs) + def __init__(self, n=False, v1=False, v2=False, m1=False, m2=False, t=False, **kwargs): + super(BeamEndRelease, self).__init__(**kwargs) self._element = None self._location = None @@ -71,7 +64,7 @@ def element(self): @element.setter def element(self, value): if not isinstance(value, compas_fea2.model.BeamElement): - raise TypeError('{!r} is not a beam element.'.format(value)) + raise TypeError("{!r} is not a beam element.".format(value)) self._element = value @property @@ -80,12 +73,12 @@ def location(self): @location.setter def location(self, value): - if not value in ('start', 'end'): - raise TypeError('the location can be either `start` or `end`') + if value not in ("start", "end"): + raise TypeError("the location can be either `start` or `end`") self._location = value -class BeamEndPinRelease(_BeamEndRelease): +class BeamEndPinRelease(BeamEndRelease): """Assign a pin end release to a `compas_fea2.model.BeamElement`. Parameters @@ -96,13 +89,14 @@ class BeamEndPinRelease(_BeamEndRelease): Release rotations about local 2 direction, by default False t : bool, optional Release rotations about local axial direction (torsion), by default False + """ - def __init__(self, m1=False, m2=False, t=False, name=None, **kwargs): - super(BeamEndPinRelease, self).__init__(n=False, v1=False, v2=False, m1=m1, m2=m2, t=t, name=name, **kwargs) + def __init__(self, m1=False, m2=False, t=False, **kwargs): + super(BeamEndPinRelease, self).__init__(n=False, v1=False, v2=False, m1=m1, m2=m2, t=t, **kwargs) -class BeamEndSliderRelease(_BeamEndRelease): +class BeamEndSliderRelease(BeamEndRelease): """Assign a slider end release to a `compas_fea2.model.BeamElement`. Parameters @@ -111,8 +105,8 @@ class BeamEndSliderRelease(_BeamEndRelease): Release displacements along local 1 direction, by default False v2 : bool, optional Release displacements along local 2 direction, by default False + """ - def __init__(self, v1=False, v2=False, name=None, **kwargs): - super(BeamEndSliderRelease, self).__init__(v1=v1, v2=v2, - n=False, m1=False, m2=False, t=False, name=name, **kwargs) + def __init__(self, v1=False, v2=False, **kwargs): + super(BeamEndSliderRelease, self).__init__(v1=v1, v2=v2, n=False, m1=False, m2=False, t=False, **kwargs) diff --git a/src/compas_fea2/model/sections.py b/src/compas_fea2/model/sections.py index 82c6bb84e..33e37f987 100644 --- a/src/compas_fea2/model/sections.py +++ b/src/compas_fea2/model/sections.py @@ -2,45 +2,39 @@ from __future__ import division from __future__ import print_function -from abc import abstractmethod from math import pi from compas_fea2 import units from compas_fea2.base import FEAData -from .materials import _Material +from .materials import Material -class _Section(FEAData): +class Section(FEAData): """Base class for sections. - Note - ---- - Sections are registered to a :class:`compas_fea2.model.Model` and can be assigned - to elements in different Parts. - Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. - material : :class:`~compas_fea2.model._Material` + material : :class:`~compas_fea2.model.Material` A material definition. Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. key : int, read-only Identifier index of the section in the parent Model. - material : :class:`~compas_fea2.model._Material` + material : :class:`~compas_fea2.model.Material` The material associated with the section. model : :class:`compas_fea2.model.Model` The model where the section is assigned. + + Notes + ----- + Sections are registered to a :class:`compas_fea2.model.Model` and can be assigned + to elements in different Parts. + """ - def __init__(self, material, name=None, **kwargs): - super(_Section, self).__init__(name=name, **kwargs) + def __init__(self, material, **kwargs): + super(Section, self).__init__(**kwargs) self._key = None self._material = material @@ -55,8 +49,8 @@ def material(self): @material.setter def material(self, value): if value: - if not isinstance(value, _Material): - raise ValueError('Material must be of type `compas_fea2.model._Material`.') + if not isinstance(value, Material): + raise ValueError("Material must be of type `compas_fea2.model.Material`.") self._material = value @property @@ -70,36 +64,35 @@ def __str__(self): model : {!r} key : {} material : {!r} -""".format(self.name, '-'*len(self.name), self.model, self.key, self.material) +""".format( + self.name, "-" * len(self.name), self.model, self.key, self.material + ) # ============================================================================== # 0D # ============================================================================== + class MassSection(FEAData): """Section for point mass elements. Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. mass : float Point mass value. Attributes ---------- - name : str - Uniqe identifier. key : int, read-only Identifier of the element in the parent part. mass : float Point mass value. + """ - def __init__(self, mass, name=None, **kwargs): - super(MassSection, self).__init__(name=name, **kwargs) + def __init__(self, mass, **kwargs): + super(MassSection, self).__init__(**kwargs) self.mass = mass self._key = None @@ -113,7 +106,9 @@ def __str__(self): --------{} model : {!r} mass : {} -""".format(self.name, '-'*len(self.name), self.model, self.mass) +""".format( + self.name, "-" * len(self.name), self.model, self.mass + ) class SpringSection(FEAData): @@ -121,9 +116,6 @@ class SpringSection(FEAData): Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. forces : dict Forces data for non-linear springs. displacements : dict @@ -133,8 +125,6 @@ class SpringSection(FEAData): Attributes ---------- - name : str - Uniqe identifier. key : int, read-only Identifier of the element in the parent part. forces : dict @@ -144,17 +134,17 @@ class SpringSection(FEAData): stiffness : dict Elastic stiffness for linear springs. - Note - ---- + Notes + ----- - Force and displacement data should range from negative to positive values. - Requires either a stiffness dict for linear springs, or forces and displacement lists for non-linear springs. - Directions are 'axial', 'lateral', 'rotation'. """ - def __init__(self, forces=None, displacements=None, stiffness=None, name=None, **kwargs): - super(SpringSection, self).__init__(name=name, **kwargs) - #TODO would be good to know the structure of these dicts and validate + def __init__(self, forces=None, displacements=None, stiffness=None, **kwargs): + super(SpringSection, self).__init__(**kwargs) + # TODO would be good to know the structure of these dicts and validate self.forces = forces or {} self.displacements = displacements or {} self.stiffness = stiffness or {} @@ -168,14 +158,17 @@ def __str__(self): forces : {} displ : {} stiffness : {} -""".format(self.name, self.forces, self.displacements, self.stiffness) +""".format( + self.name, self.forces, self.displacements, self.stiffness + ) # ============================================================================== # 1D # ============================================================================== -class BeamSection(_Section): + +class BeamSection(Section): """Custom section for beam elements. Parameters @@ -198,11 +191,8 @@ class BeamSection(_Section): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -224,16 +214,13 @@ class BeamSection(_Section): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, *, A, Ixx, Iyy, Ixy, Avx, Avy, J, g0, gw, material, name=None, **kwargs): - super(BeamSection, self).__init__(material=material, name=name, **kwargs) + def __init__(self, *, A, Ixx, Iyy, Ixy, Avx, Avy, J, g0, gw, material, **kwargs): + super(BeamSection, self).__init__(material=material, **kwargs) self.A = A self.Ixx = Ixx self.Iyy = Iyy @@ -260,28 +247,26 @@ def __str__(self): J : {} g0 : {} gw : {} -""".format(self.__class__.__name__, - len(self.__class__.__name__) * '-', - self.name, - self.material, - (self.A * units['m**2']), - (self.Ixx * units['m**4']), - (self.Iyy * units['m**4']), - (self.Ixy * units['m**4']), - (self.Avx * units['m**2']), - (self.Avy * units['m**2']), - self.J, - self.g0, - self.gw) +""".format( + self.__class__.__name__, + len(self.__class__.__name__) * "-", + self.name, + self.material, + (self.A * units["m**2"]), + (self.Ixx * units["m**4"]), + (self.Iyy * units["m**4"]), + (self.Ixy * units["m**4"]), + (self.Avx * units["m**2"]), + (self.Avy * units["m**2"]), + self.J, + self.g0, + self.gw, + ) class AngleSection(BeamSection): """Uniform thickness angle cross-section for beam elements. - Warning - ------- - - Ixy not yet calculated. - Parameters ---------- w : float @@ -290,7 +275,7 @@ class AngleSection(BeamSection): Height. t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. name : str, optional Section name. If not provided, a unique identifier is automatically @@ -322,49 +307,56 @@ class AngleSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. name : str Section name. If not provided, a unique identifier is automatically assigned. + Warnings + -------- + - Ixy not yet calculated. + """ - def __init__(self, w, h, t, material, name=None, **kwargs): + def __init__(self, w, h, t, material, **kwargs): self.w = w self.h = h self.t = t - p = 2. * (w + h - t) + p = 2.0 * (w + h - t) xc = (w**2 + h * t - t**2) / p yc = (h**2 + w * t - t**2) / p A = t * (w + h - t) - Ixx = (1. / 3) * (w * h**3 - (w - t) * (h - t)**3) - self.A * (h - yc)**2 - Iyy = (1. / 3) * (h * w**3 - (h - t) * (w - t)**3) - self.A * (w - xc)**2 + Ixx = (1.0 / 3) * (w * h**3 - (w - t) * (h - t) ** 3) - self.A * (h - yc) ** 2 + Iyy = (1.0 / 3) * (h * w**3 - (h - t) * (w - t) ** 3) - self.A * (w - xc) ** 2 Ixy = 0 - J = (1. / 3) * (h + w - t) * t**3 + J = (1.0 / 3) * (h + w - t) * t**3 Avx = 0 Avy = 0 g0 = 0 gw = 0 - super(AngleSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(AngleSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) # TODO implement different thickness along the 4 sides class BoxSection(BeamSection): """Hollow rectangular box cross-section for beam elements. - Note - ---- - Currently you can only specify the thickness of the flanges and the webs. - - Warning - ------- - - Ixy not yet calculated. - Parameters ---------- w : float @@ -375,11 +367,8 @@ class BoxSection(BeamSection): Web thickness. tf : float Flange thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -409,15 +398,20 @@ class BoxSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + + Notes + ----- + Currently you can only specify the thickness of the flanges and the webs. + + Warnings + -------- + - Ixy not yet calculated. """ - def __init__(self, w, h, tw, tf, material, name=None, **kwargs): + def __init__(self, w, h, tw, tf, material, **kwargs): self.w = w self.h = h self.tw = tw @@ -427,8 +421,8 @@ def __init__(self, w, h, tw, tf, material, name=None, **kwargs): p = 2 * ((h - tf) / tw + (w - tw) / tf) A = w * h - (w - 2 * tw) * (h - 2 * tf) - Ixx = (w * h**3) / 12. - ((w - 2 * tw) * (h - 2 * tf)**3) / 12. - Iyy = (h * w**3) / 12. - ((h - 2 * tf) * (w - 2 * tw)**3) / 12. + Ixx = (w * h**3) / 12.0 - ((w - 2 * tw) * (h - 2 * tf) ** 3) / 12.0 + Iyy = (h * w**3) / 12.0 - ((h - 2 * tf) * (w - 2 * tw) ** 3) / 12.0 Ixy = 0 Avx = 0 Avy = 0 @@ -436,8 +430,19 @@ def __init__(self, w, h, tw, tf, material, name=None, **kwargs): g0 = 0 gw = 0 - super(BoxSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(BoxSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class CircularSection(BeamSection): @@ -447,11 +452,8 @@ class CircularSection(BeamSection): ---------- r : float Radius. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -475,19 +477,17 @@ class CircularSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, r, material, name=None, **kwargs): + def __init__(self, r, material, **kwargs): self.r = r D = 2 * r A = 0.25 * pi * D**2 - Ixx = Iyy = (pi * D**4) / 64. + Ixx = Iyy = (pi * D**4) / 64.0 Ixy = 0 Avx = 0 Avy = 0 @@ -495,8 +495,19 @@ def __init__(self, r, material, name=None, **kwargs): g0 = 0 gw = 0 - super(CircularSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(CircularSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class HexSection(BeamSection): @@ -535,24 +546,18 @@ class HexSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, r, t, material, name=None, **kwargs): - raise NotImplementedError('This section is not available for the selected backend') + def __init__(self, r, t, material, **kwargs): + raise NotImplementedError("This section is not available for the selected backend") class ISection(BeamSection): """Equal flanged I-section for beam elements. - Note - ---- - Currently you the thickness of the two flanges is the same. - Parameters ---------- w : float @@ -563,11 +568,8 @@ class ISection(BeamSection): Web thickness. tf : float Flange thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -597,31 +599,44 @@ class ISection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + + Notes + ----- + Currently you the thickness of the two flanges is the same. + """ - def __init__(self, w, h, tw, tf, material, name=None, **kwargs): + def __init__(self, w, h, tw, tf, material, **kwargs): self.w = w self.h = h self.tw = tw self.tf = tf A = 2 * w * tf + (h - 2 * tf) * tw - Ixx = (tw * (h - 2 * tf)**3) / 12. + 2 * ((tf**3) * w / 12. + w * tf * (h / 2. - tf / 2.)**2) - Iyy = ((h - 2 * tf) * tw**3) / 12. + 2 * ((w**3) * tf / 12.) + Ixx = (tw * (h - 2 * tf) ** 3) / 12.0 + 2 * ((tf**3) * w / 12.0 + w * tf * (h / 2.0 - tf / 2.0) ** 2) + Iyy = ((h - 2 * tf) * tw**3) / 12.0 + 2 * ((w**3) * tf / 12.0) Ixy = 0 Avx = 0 Avy = 0 - J = (1. / 3) * (2 * w * tf**3 + (h - tf) * tw**3) + J = (1.0 / 3) * (2 * w * tf**3 + (h - tf) * tw**3) g0 = 0 gw = 0 - super(ISection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(ISection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class PipeSection(BeamSection): @@ -633,11 +648,8 @@ class PipeSection(BeamSection): Outer radius. t : float Wall thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -663,30 +675,39 @@ class PipeSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, r, t, material, name=None, **kwargs): + def __init__(self, r, t, material, **kwargs): self.r = r self.t = t D = 2 * r - A = 0.25 * pi * (D**2 - (D - 2 * t)**2) - Ixx = Iyy = 0.25 * pi * (r**4 - (r - t)**4) + A = 0.25 * pi * (D**2 - (D - 2 * t) ** 2) + Ixx = Iyy = 0.25 * pi * (r**4 - (r - t) ** 4) Ixy = 0 Avx = 0 Avy = 0 - J = (2. / 3) * pi * (r + 0.5 * t) * t**3 + J = (2.0 / 3) * pi * (r + 0.5 * t) * t**3 g0 = 0 gw = 0 - super(PipeSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(PipeSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class RectangularSection(BeamSection): @@ -698,11 +719,8 @@ class RectangularSection(BeamSection): Width. h : float Height. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -728,15 +746,12 @@ class RectangularSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, w, h, material, name=None, **kwargs): + def __init__(self, w, h, material, **kwargs): self.w = w self.h = h @@ -744,8 +759,8 @@ def __init__(self, w, h, material, name=None, **kwargs): l2 = min([w, h]) A = w * h - Ixx = (1 / 12.) * w * h**3 - Iyy = (1 / 12.) * h * w**3 + Ixx = (1 / 12.0) * w * h**3 + Iyy = (1 / 12.0) * h * w**3 Ixy = 0 Avy = 0.833 * A Avx = 0.833 * A @@ -753,17 +768,24 @@ def __init__(self, w, h, material, name=None, **kwargs): g0 = 0 gw = 0 - super(RectangularSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(RectangularSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class TrapezoidalSection(BeamSection): """Solid trapezoidal cross-section for beam elements. - Warning - ------- - - J not yet calculated. - Parameters ---------- w1 : float @@ -772,11 +794,8 @@ class TrapezoidalSection(BeamSection): Width at top. h : float Height. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -804,15 +823,16 @@ class TrapezoidalSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + + Warnings + -------- + - J not yet calculated. """ - def __init__(self, w1, w2, h, material, name=None, **kwargs): + def __init__(self, w1, w2, h, material, **kwargs): self.w1 = w1 self.w2 = w2 self.h = h @@ -820,8 +840,8 @@ def __init__(self, w1, w2, h, material, name=None, **kwargs): # c = (h * (2 * w2 + w1)) / (3. * (w1 + w2)) # NOTE: not used A = 0.5 * (w1 + w2) * h - Ixx = (1 / 12.) * (3 * w2 + w1) * h**3 - Iyy = (1 / 48.) * h * (w1 + w2) * (w2**2 + 7 * w1**2) + Ixx = (1 / 12.0) * (3 * w2 + w1) * h**3 + Iyy = (1 / 48.0) * h * (w1 + w2) * (w2**2 + 7 * w1**2) Ixy = 0 Avx = 0 Avy = 0 @@ -829,8 +849,19 @@ def __init__(self, w1, w2, h, material, name=None, **kwargs): g0 = 0 gw = 0 - super(TrapezoidalSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(TrapezoidalSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class TrussSection(BeamSection): @@ -840,11 +871,8 @@ class TrussSection(BeamSection): ---------- A : float Area. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -866,15 +894,12 @@ class TrussSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, A, material, name=None, **kwargs): + def __init__(self, A, material, **kwargs): Ixx = 0 Iyy = 0 Ixy = 0 @@ -883,8 +908,19 @@ def __init__(self, A, material, name=None, **kwargs): J = 0 g0 = 0 gw = 0 - super(TrussSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(TrussSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class StrutSection(TrussSection): @@ -894,11 +930,8 @@ class StrutSection(TrussSection): ---------- A : float Area. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -920,16 +953,13 @@ class StrutSection(TrussSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, A, material, name=None, **kwargs): - super(StrutSection, self).__init__(A=A, material=material, name=name, **kwargs) + def __init__(self, A, material, **kwargs): + super(StrutSection, self).__init__(A=A, material=material, **kwargs) class TieSection(TrussSection): @@ -939,11 +969,8 @@ class TieSection(TrussSection): ---------- A : float Area. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -965,78 +992,65 @@ class TieSection(TrussSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, A, material, name=None, **kwargs): - super(TieSection, self).__init__(A=A, material=material, name=name, **kwargs) + def __init__(self, A, material, **kwargs): + super(TieSection, self).__init__(A=A, material=material, **kwargs) # ============================================================================== # 2D # ============================================================================== -class ShellSection(_Section): + +class ShellSection(Section): """Section for shell elements. Parameters ---------- t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, t, material, name=None, **kwargs): - super(ShellSection, self).__init__(material=material, name=name, **kwargs) + def __init__(self, t, material, **kwargs): + super(ShellSection, self).__init__(material=material, **kwargs) self.t = t -class MembraneSection(_Section): +class MembraneSection(Section): """Section for membrane elements. Parameters ---------- t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, t, material, name=None, **kwargs): - super(MembraneSection, self).__init__(material=material, name=name, **kwargs) + def __init__(self, t, material, **kwargs): + super(MembraneSection, self).__init__(material=material, **kwargs) self.t = t @@ -1044,25 +1058,21 @@ def __init__(self, t, material, name=None, **kwargs): # 3D # ============================================================================== -class SolidSection(_Section): + +class SolidSection(Section): """Section for solid elements. Parameters ---------- - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, material, name=None, **kwargs): - super(SolidSection, self).__init__(material=material, name=name, **kwargs) + def __init__(self, material, **kwargs): + super(SolidSection, self).__init__(material=material, **kwargs) diff --git a/src/compas_fea2/postprocess/__init__.py b/src/compas_fea2/postprocess/__init__.py index c6a1fea46..eb8d2f3ee 100644 --- a/src/compas_fea2/postprocess/__init__.py +++ b/src/compas_fea2/postprocess/__init__.py @@ -1,19 +1,3 @@ -""" -******************************************************************************** -postprocess -******************************************************************************** - -.. currentmodule:: compas_fea2.postprocess - -Stresses -======== - -.. autosummary:: - :toctree: generated/ - - principal_stresses - -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -22,5 +6,5 @@ __all__ = [ - 'principal_stresses', + "principal_stresses", ] diff --git a/src/compas_fea2/postprocess/stresses.py b/src/compas_fea2/postprocess/stresses.py index 64017da9d..1d576c374 100644 --- a/src/compas_fea2/postprocess/stresses.py +++ b/src/compas_fea2/postprocess/stresses.py @@ -6,7 +6,7 @@ def principal_stresses(data): - """ Performs principal stress calculations solving the eigenvalues problem. + """Performs principal stress calculations solving the eigenvalues problem. Parameters ---------- @@ -28,10 +28,11 @@ def principal_stresses(data): Warnings -------- The function is experimental and works only for shell elements at the moment. + """ - components = ['sxx', 'sxy', 'syy'] - stype = ['max', 'min'] - section_points = ['sp1', 'sp5'] + components = ["sxx", "sxy", "syy"] + stype = ["max", "min"] + section_points = ["sp1", "sp5"] stress_results = list(zip(*[data[stress_name].values() for stress_name in components])) array_size = ((len(stress_results)), (2, len(stress_results))) @@ -42,8 +43,7 @@ def principal_stresses(data): # Stresses are computed as mean of the values at each integration points stress_vector = [np.mean(np.array([v for k, v in i.items() if sp in k])) for i in element_stresses] # The principal stresses and their directions are computed solving the eigenvalues problem - stress_matrix = np.array([(stress_vector[0], stress_vector[1]), - (stress_vector[1], stress_vector[2])]) + stress_matrix = np.array([(stress_vector[0], stress_vector[1]), (stress_vector[1], stress_vector[2])]) w_sp, v_sp = np.linalg.eig(stress_matrix) # sort by larger to smaller eigenvalue idx = w_sp.argsort()[::-1] diff --git a/src/compas_fea2/problem/__init__.py b/src/compas_fea2/problem/__init__.py index e0320f0c1..0906070c6 100644 --- a/src/compas_fea2/problem/__init__.py +++ b/src/compas_fea2/problem/__init__.py @@ -1,86 +1,3 @@ -""" -******************************************************************************** -problem -******************************************************************************** - -.. currentmodule:: compas_fea2.problem - -Problem -======= - -.. autosummary:: - :toctree: generated/ - - Problem - -Steps -===== - -.. autosummary:: - :toctree: generated/ - - _Step - _GeneralStep - _Perturbation - ModalAnalysis - ComplexEigenValue - StaticStep - LinearStaticPerturbation - BucklingAnalysis - DynamicStep - QuasiStaticStep - DirectCyclicStep - -Prescribed Fields -================= - -.. autosummary:: - :toctree: generated/ - - _PrescribedField - PrescribedTemperatureField - -Loads -===== - -.. autosummary:: - :toctree: generated/ - - _Load - PrestressLoad - PointLoad - LineLoad - AreaLoad - GravityLoad - TributaryLoad - HarmonicPointLoad - HarmonicPressureLoad - ThermalLoad - -Displacements -============= - -.. autosummary:: - :toctree: generated/ - - GeneralDisplacement - -Load Patterns -============= -.. autosummary:: - :toctree: generated/ - - Pattern - -Outputs -======= - -.. autosummary:: - :toctree: generated/ - - FieldOutput - HistoryOutput -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -88,7 +5,7 @@ from .problem import Problem from .displacements import GeneralDisplacement from .loads import ( - _Load, + Load, PrestressLoad, PointLoad, LineLoad, @@ -99,67 +16,46 @@ HarmonicPressureLoad, ThermalLoad, ) -from .fields import ( - _PrescribedField, - PrescribedTemperatureField, -) - -from .patterns import ( - Pattern, -) -from .steps import ( - _Step, - _GeneralStep, - _Perturbation, - ModalAnalysis, - ComplexEigenValue, - StaticStep, - LinearStaticPerturbation, - BucklingAnalysis, - DynamicStep, - QuasiStaticStep, - DirectCyclicStep, -) +from .fields import PrescribedField, PrescribedTemperatureField +from .patterns import Pattern +from .steps.step import Step, GeneralStep +from .steps.dynamic import DynamicStep +from .steps.perturbations import Perturbation, ModalAnalysis, ComplexEigenValue, BucklingAnalysis +from .steps.quasistatic import QuasiStaticStep, DirectCyclicStep +from .steps.static import StaticStep -from .outputs import ( - FieldOutput, - HistoryOutput -) +from .outputs import FieldOutput, HistoryOutput __all__ = [ - 'Problem', - - 'GeneralDisplacement', - - '_Load', - 'PrestressLoad', - 'PointLoad', - 'LineLoad', - 'AreaLoad', - 'GravityLoad', - 'TributaryLoad', - 'HarmonicPointLoad', - 'HarmonicPressureLoad', - 'ThermalLoad', - - 'PrescribedTemperatureField', - - 'DeadLoad', - 'LiveLoad', - 'SuperImposedDeadLoad', - - '_Step', - '_GeneralStep', - '_Perturbation', - 'ModalAnalysis', - 'ComplexEigenValue', - 'StaticStep', - 'LinearStaticPerturbation', - 'BucklingAnalysis', - 'DynamicStep', - 'QuasiStaticStep', - 'DirectCyclicStep', - - 'FieldOutput', - 'HistoryOutput', + "Problem", + "GeneralDisplacement", + "Load", + "PrestressLoad", + "PointLoad", + "LineLoad", + "AreaLoad", + "GravityLoad", + "TributaryLoad", + "HarmonicPointLoad", + "HarmonicPressureLoad", + "ThermalLoad", + "Pattern", + "PrescribedField", + "PrescribedTemperatureField", + "DeadLoad", + "LiveLoad", + "SuperImposedDeadLoad", + "Step", + "GeneralStep", + "Perturbation", + "ModalAnalysis", + "ComplexEigenValue", + "StaticStep", + "LinearStaticPerturbation", + "BucklingAnalysis", + "DynamicStep", + "QuasiStaticStep", + "DirectCyclicStep", + "FieldOutput", + "HistoryOutput", ] diff --git a/src/compas_fea2/problem/displacements.py b/src/compas_fea2/problem/displacements.py index 836acf8ca..54be3db8d 100644 --- a/src/compas_fea2/problem/displacements.py +++ b/src/compas_fea2/problem/displacements.py @@ -8,10 +8,6 @@ class GeneralDisplacement(FEAData): """GeneralDisplacement object. - Note - ---- - Displacements are registered to a :class:`compas_fea2.problem.Step`. - Parameters ---------- name : str, optional @@ -51,9 +47,14 @@ class GeneralDisplacement(FEAData): zz component of moment, by default 0. axes : str, optional BC applied via 'local' or 'global' axes, by default 'global'. + + Notes + ----- + Displacements are registered to a :class:`compas_fea2.problem.Step`. + """ - def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes='global', name=None, **kwargs): + def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes="global", name=None, **kwargs): super(GeneralDisplacement, self).__init__(name=name, **kwargs) self.x = x self.y = y @@ -73,4 +74,4 @@ def axes(self, value): @property def components(self): - return {c: getattr(self, c) for c in ['x', 'y', 'z', 'xx', 'yy', 'zz']} + return {c: getattr(self, c) for c in ["x", "y", "z", "xx", "yy", "zz"]} diff --git a/src/compas_fea2/problem/fields.py b/src/compas_fea2/problem/fields.py index fafd5cc86..be8a9acab 100644 --- a/src/compas_fea2/problem/fields.py +++ b/src/compas_fea2/problem/fields.py @@ -5,21 +5,21 @@ from compas_fea2.base import FEAData -class _PrescribedField(FEAData): +class PrescribedField(FEAData): """Base class for all predefined initial conditions. - Note - ---- + Notes + ----- Fields are registered to a :class:`compas_fea2.problem.Step`. + """ def __init__(self, name=None, **kwargs): - super(_PrescribedField, self).__init__(name=name, **kwargs) + super(PrescribedField, self).__init__(name=name, **kwargs) -class PrescribedTemperatureField(_PrescribedField): - """Temperature field - """ +class PrescribedTemperatureField(PrescribedField): + """Temperature field""" def __init__(self, temperature, name=None, **kwargs): super(PrescribedTemperatureField, self).__init__(name, **kwargs) diff --git a/src/compas_fea2/problem/loads.py b/src/compas_fea2/problem/loads.py index 6cc3f280f..4c5cc760d 100644 --- a/src/compas_fea2/problem/loads.py +++ b/src/compas_fea2/problem/loads.py @@ -7,13 +7,9 @@ # TODO: make units independent using the utilities function -class _Load(FEAData): +class Load(FEAData): """Initialises base Load object. - Note - ---- - Loads are registered to a :class:`compas_fea2.problem.Pattern`. - Parameters ---------- name : str @@ -33,10 +29,15 @@ class _Load(FEAData): Load components. These differ according to each Load type axes : str, optional Load applied via 'local' or 'global' axes, by default 'global'. + + Notes + ----- + Loads are registered to a :class:`compas_fea2.problem.Pattern`. + """ - def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): - super(_Load, self).__init__(name=name, **kwargs) + def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", name=None, **kwargs): + super(Load, self).__init__(name=name, **kwargs) self._axes = axes self.x = x self.y = y @@ -47,11 +48,11 @@ def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='glob def __rmul__(self, other): if isinstance(other, (float, int)): - components = ['x', 'y', 'z', 'xx', 'yy', 'zz'] + components = ["x", "y", "z", "xx", "yy", "zz"] for component in components: value = getattr(self, component) if value: - setattr(self, component, other*value) + setattr(self, component, other * value) return self @property @@ -64,7 +65,7 @@ def axes(self, value): @property def components(self): - keys = ['x', 'y', 'z', 'xx', 'yy', 'zz'] + keys = ["x", "y", "z", "xx", "yy", "zz"] return {key: getattr(self, key) for key in keys} @components.setter @@ -88,7 +89,8 @@ def problem(self): def model(self): return self.problem._registration -class PointLoad(_Load): + +class PointLoad(Load): """Concentrated forces and moments [units:N, Nm] applied to node(s). Parameters @@ -129,11 +131,11 @@ class PointLoad(_Load): Load applied via 'local' or 'global' axes. """ - def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): + def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", name=None, **kwargs): super(PointLoad, self).__init__(x=x, y=y, z=z, xx=xx, yy=yy, zz=zz, axes=axes, name=name, **kwargs) -class LineLoad(_Load): +class LineLoad(Load): """Distributed line forces and moments [units:N/m or Nm/m] applied to element(s). Parameters @@ -177,12 +179,13 @@ class LineLoad(_Load): Load applied via 'local' or 'global' axes, by default 'global'. """ - def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes='global', name=None, **kwargs): - super(LineLoad, self).__init__(components={ - 'x': x, 'y': y, 'z': z, 'xx': xx, 'yy': yy, 'zz': zz}, axes=axes, name=name, **kwargs) + def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes="global", name=None, **kwargs): + super(LineLoad, self).__init__( + components={"x": x, "y": y, "z": z, "xx": xx, "yy": yy, "zz": zz}, axes=axes, name=name, **kwargs + ) -class AreaLoad(_Load): +class AreaLoad(Load): """Distributed area force [e.g. units:N/m2] applied to element(s). Parameters @@ -211,17 +214,13 @@ class AreaLoad(_Load): z component of area load. """ - def __init__(self, x=0, y=0, z=0, axes='local', name=None, **kwargs): - super(AreaLoad, self).__init__(components={'x': x, 'y': y, 'z': z}, axes=axes, name=name, **kwargs) + def __init__(self, x=0, y=0, z=0, axes="local", name=None, **kwargs): + super(AreaLoad, self).__init__(components={"x": x, "y": y, "z": z}, axes=axes, name=name, **kwargs) -class GravityLoad(_Load): +class GravityLoad(Load): """Gravity load [units:N/m3] applied to element(s). - Note - ---- - By default gravity is supposed to act along the negative `z` axis. - Parameters ---------- elements : str, list @@ -250,10 +249,15 @@ class GravityLoad(_Load): Factor to apply to y direction. z : float Factor to apply to z direction. + + Notes + ----- + By default gravity is supposed to act along the negative `z` axis. + """ def __init__(self, g, x=0, y=0, z=-1, name=None, **kwargs): - super(GravityLoad, self).__init__(x=x, y=y, z=z, axes='global', name=name, **kwargs) + super(GravityLoad, self).__init__(x=x, y=y, z=z, axes="global", name=name, **kwargs) self._g = g @property @@ -261,40 +265,40 @@ def g(self): return self._g -class PrestressLoad(_Load): +class PrestressLoad(Load): """Prestress load""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(TributaryLoad, self).__init__(components, axes, name, **kwargs) raise NotImplementedError -class ThermalLoad(_Load): +class ThermalLoad(Load): """Thermal load""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(ThermalLoad, self).__init__(components, axes, name, **kwargs) -class TributaryLoad(_Load): +class TributaryLoad(Load): """Tributary load""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(TributaryLoad, self).__init__(components, axes, name, **kwargs) raise NotImplementedError -class HarmonicPointLoad(_Load): +class HarmonicPointLoad(Load): """""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(HarmonicPointLoad, self).__init__(components, axes, name, **kwargs) raise NotImplementedError -class HarmonicPressureLoad(_Load): +class HarmonicPressureLoad(Load): """""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(HarmonicPressureLoad, self).__init__(components, axes, name, **kwargs) raise NotImplementedError diff --git a/src/compas_fea2/problem/outputs.py b/src/compas_fea2/problem/outputs.py index 57f5f5884..3122c72af 100644 --- a/src/compas_fea2/problem/outputs.py +++ b/src/compas_fea2/problem/outputs.py @@ -6,21 +6,22 @@ from itertools import chain -class _Output(FEAData): +class Output(FEAData): """Base class for output requests. - Note - ---- - Outputs are registered to a :class:`compas_fea2.problem._Step`. - Parameters ---------- FEAData : _type_ _description_ + + Notes + ----- + Outputs are registered to a :class:`compas_fea2.problem.Step`. + """ def __init__(self, name=None, **kwargs): - super(_Output, self).__init__(name=name, **kwargs) + super(Output, self).__init__(name=name, **kwargs) @property def step(self): @@ -34,7 +35,8 @@ def problem(self): def model(self): return self.problem._registration -class FieldOutput(_Output): + +class FieldOutput(Output): """FieldOutput object for specification of the fields (stresses, displacements, etc..) to output from the analysis. @@ -54,6 +56,7 @@ class FieldOutput(_Output): list of node fields to output elements_outputs : list list of elements fields to output + """ def __init__(self, node_outputs=None, element_outputs=None, contact_outputs=None, name=None, **kwargs): @@ -78,7 +81,8 @@ def contact_outputs(self): def outputs(self): return chain(self.node_outputs, self.element_outputs, self.contact_outputs) -class HistoryOutput(_Output): + +class HistoryOutput(Output): """HistoryOutput object for recording the fields (stresses, displacements, etc..) from the analysis. @@ -93,7 +97,8 @@ class HistoryOutput(_Output): name : str Uniqe identifier. If not provided it is automatically generated. Set a name if you want a more human-readable input file. + """ - def __init__(self, name=None, **kwargs): + def __init__(self, name=None, **kwargs): super(HistoryOutput, self).__init__(name=name, **kwargs) diff --git a/src/compas_fea2/problem/patterns.py b/src/compas_fea2/problem/patterns.py index 002c3415a..3cccbdb96 100644 --- a/src/compas_fea2/problem/patterns.py +++ b/src/compas_fea2/problem/patterns.py @@ -8,32 +8,33 @@ class Pattern(FEAData): + """A pattern is the spatial distribution of a specific set of forces, + displacements, temperatures, and other effects which act on a structure. + Any combination of nodes and elements may be subjected to loading and + kinematic conditions. + + Parameters + ---------- + value : :class:`compas_fea2.problem.Load` | :class:`compas_fea2.problem.GeneralDisplacement` + The load/displacement of the pattern + distribution : list + list of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model.Element` + name : str + Uniqe identifier. If not provided it is automatically generated. Set a + name if you want a more human-readable input file. + + Attributes + ---------- + value : :class:`compas_fea2.problem.Load` + The load of the pattern + distribution : list + list of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model.Element` + name : str + Uniqe identifier. + + """ def __init__(self, value, distribution, name=None, **kwargs): - """A pattern is the spatial distribution of a specific set of forces, - displacements, temperatures, and other effects which act on a structure. - Any combination of nodes and elements may be subjected to loading and - kinematic conditions. - - Parameters - ---------- - value : :class:`compas_fea2.problem._Load` | :class:`compas_fea2.problem.GeneralDisplacement` - The load/displacement of the pattern - distribution : list - list of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model._Element` - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. - - Attributes - ---------- - value : :class:`compas_fea2.problem._Load` - The load of the pattern - distribution : list - list of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model._Element` - name : str - Uniqe identifier. - """ super(Pattern, self).__init__(name, **kwargs) self._load = value value._registration = self diff --git a/src/compas_fea2/problem/problem.py b/src/compas_fea2/problem/problem.py index c1207e41f..eabfd9d9b 100644 --- a/src/compas_fea2/problem/problem.py +++ b/src/compas_fea2/problem/problem.py @@ -2,52 +2,25 @@ from __future__ import division from __future__ import print_function -import pickle +import os import compas_fea2 from pathlib import Path -import os from typing import Iterable -from unittest import result + +from compas.geometry import Vector +from compas.geometry import sum_vectors from compas_fea2.base import FEAData -from compas_fea2.problem.steps.step import _Step +from compas_fea2.problem.steps.step import Step from compas_fea2.job.input_file import InputFile - from compas_fea2.utilities._utils import timer -from compas_fea2.utilities._utils import step_method - from compas_fea2.results import NodeFieldResults -from compas.geometry import Point, Plane -from compas.geometry import Vector -from compas.geometry import sum_vectors - - class Problem(FEAData): - """A Problem is a collection of analysis steps (:class:`compas_fea2.problem._Step) + """A Problem is a collection of analysis steps (:class:`compas_fea2.problem.Step) applied in a specific sequence. - Note - ---- - Problems are registered to a :class:`compas_fea2.model.Model`. - - Problems can also be used as canonical `load combinations`, where each `load` - is actually a `factored step`. For example, a typical load combination such - as 1.35*DL+1.50LL can be applied to the model by creating the Steps DL and LL, - factoring them (see :class:`compas_fea2.problem._Step documentation) and adding - them to Problme - - Note - ---- - While for linear models the sequence of the steps is irrelevant, it is not the - case for non-linear models. - - Warning - ------- - Factore Steps are new objects! check the :class:`compas_fea2.problem._Step - documentation. - Parameters ---------- name : str, optional @@ -67,13 +40,31 @@ class Problem(FEAData): describption : str Brief description of the Problem. This will be added to the input file and can be useful for future reference. - steps : list of :class:`compas_fea2.problem._Step` + steps : list of :class:`compas_fea2.problem.Step` list of analysis steps in the order they are applied. path : str, :class:`pathlib.Path` Path to the analysis folder where all the files will be saved. results : :class:`compas_fea2.results.Results` Results object with the analyisis results. + Notes + ----- + Problems are registered to a :class:`compas_fea2.model.Model`. + + Problems can also be used as canonical `load combinations`, where each `load` + is actually a `factored step`. For example, a typical load combination such + as 1.35*DL+1.50LL can be applied to the model by creating the Steps DL and LL, + factoring them (see :class:`compas_fea2.problem.Step documentation) and adding + them to Problme + + While for linear models the sequence of the steps is irrelevant, it is not the + case for non-linear models. + + Warnings + -------- + Factore Steps are new objects! check the :class:`compas_fea2.problem.Step + documentation. + """ def __init__(self, name=None, description=None, **kwargs): @@ -96,10 +87,11 @@ def steps(self): @property def path(self): return self._path + @path.setter def path(self, value): self._path = value if isinstance(value, Path) else Path(value) - self._path_db = os.path.join(self._path, '{}-results.db'.format(self.name)) + self._path_db = os.path.join(self._path, "{}-results.db".format(self.name)) @property def db_connection(self): @@ -112,19 +104,20 @@ def path_db(self): @property def steps_order(self): return self._steps_order + @steps_order.setter def steps_order(self, value): for step in value: if not self.is_step_in_problem(step, add=False): - raise ValueError('{!r} must be previously added to {!r}'.format(step, self)) + raise ValueError("{!r} must be previously added to {!r}".format(step, self)) self._steps_order = value - # ========================================================================= # Step methods # ========================================================================= + def find_step_by_name(self, name): - # type: (str) -> _Step + # type: (str) -> Step """Find if there is a step with the given name in the problem. Parameters @@ -133,7 +126,7 @@ def find_step_by_name(self, name): Returns ------- - :class:`compas_fea2.problem._Step` + :class:`compas_fea2.problem.Step` """ for step in self.steps: @@ -141,16 +134,16 @@ def find_step_by_name(self, name): return step def is_step_in_problem(self, step, add=True): - """Check if a :class:`compas_fea2.problem._Step` is defined in the Problem. + """Check if a :class:`compas_fea2.problem.Step` is defined in the Problem. Parameters ---------- - step : :class:`compas_fea2.problem._Step` + step : :class:`compas_fea2.problem.Step` The Step object to find. Returns ------- - :class:`compas_fea2.problem._Step` + :class:`compas_fea2.problem.Step` Raises ------ @@ -161,36 +154,36 @@ def is_step_in_problem(self, step, add=True): name of a Step already defined in the Problem. """ - if not isinstance(step, _Step): - raise TypeError('{!r} is not a Step'.format(step)) + if not isinstance(step, Step): + raise TypeError("{!r} is not a Step".format(step)) if step not in self.steps: - print('{!r} not found'.format(step)) + print("{!r} not found".format(step)) if add: step = self.add_step(step) - print('{!r} added to the Problem'.format(step)) + print("{!r} added to the Problem".format(step)) return step return False return True def add_step(self, step): # # type: (Step) -> Step - """Adds a :class:`compas_fea2.problem._Step` to the problem. The name of + """Adds a :class:`compas_fea2.problem.Step` to the problem. The name of the Step must be unique Parameters ---------- - Step : :class:`compas_fea2.problem._Step` + Step : :class:`compas_fea2.problem.Step` The analysis step to add to the problem. Returns ------- - :class:`compas_fea2.problem._Step` + :class:`compas_fea2.problem.Step` """ - if not isinstance(step, _Step): - raise TypeError('You must provide a valid compas_fea2 Step object') + if not isinstance(step, Step): + raise TypeError("You must provide a valid compas_fea2 Step object") if self.find_step_by_name(step): - raise ValueError('There is already a step with the same name in the model.') + raise ValueError("There is already a step with the same name in the model.") step._key = len(self._steps) self._steps.add(step) @@ -199,16 +192,16 @@ def add_step(self, step): return step def add_steps(self, steps): - """Adds multiple :class:`compas_fea2.problem._Step` objects to the problem. + """Adds multiple :class:`compas_fea2.problem.Step` objects to the problem. Parameters ---------- - steps : list[:class:`compas_fea2.problem._Step`] + steps : list[:class:`compas_fea2.problem.Step`] List of steps objects in the order they will be applied. Returns ------- - list[:class:`compas_fea2.problem._Step`] + list[:class:`compas_fea2.problem.Step`] """ return [self.add_step(step) for step in steps] @@ -230,18 +223,13 @@ def add_steps(self, steps): # Not implemented yet! # """ # for step in order: - # if not isinstance(step, _Step): + # if not isinstance(step, Step): # raise TypeError('{} is not a step'.format(step)) # self._steps_order = order def add_linear_perturbation_step(self, lp_step, base_step): """Add a linear perturbation step to a previously defined step. - Note - ---- - Linear perturbartion steps do not change the history of the problem (hence - following steps will not consider their effects). - Parameters ---------- lp_step : obj @@ -249,6 +237,12 @@ def add_linear_perturbation_step(self, lp_step, base_step): base_step : str name of a previously defined step which will be used as starting conditions for the application of the linear perturbation step. + + Notes + ----- + Linear perturbartion steps do not change the history of the problem (hence + following steps will not consider their effects). + """ raise NotImplementedError @@ -269,7 +263,7 @@ def summary(self): str Problem summary """ - steps_data = '\n'.join([f'{step.name}' for step in self.steps]) + steps_data = "\n".join([f"{step.name}" for step in self.steps]) summary = """ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -284,17 +278,17 @@ def summary(self): Analysis folder path : {} -""".format(self._name, - self.description or 'N/A', - steps_data, - self.path or 'N/A') +""".format( + self._name, self.description or "N/A", steps_data, self.path or "N/A" + ) print(summary) return summary # ========================================================================= # Analysis methods # ========================================================================= - @timer(message='Finished writing input file in') + + @timer(message="Finished writing input file in") def write_input_file(self, path=None): # type: (Path |str) -> None """Writes the input file. @@ -325,15 +319,17 @@ def _check_analysis_path(self, path): path : :class:`pathlib.Path` Path where the input file will be saved. - Return + Returns + ------- :class:`pathlib.Path` Path where the input file will be saved. + """ if path: self.model.path = path self.path = self.model.path.joinpath(self.name) if not self.path and not self.model.path: - raise AttributeError('You must provide a path for storing the model and the analysis results.') + raise AttributeError("You must provide a path for storing the model and the analysis results.") return self.path def analyse(self, path=None, *args, **kwargs): @@ -343,12 +339,12 @@ def analyse(self, path=None, *args, **kwargs): ------ NotImplementedError This method is implemented only at the backend level. + """ raise NotImplementedError("this function is not available for the selected backend") def analyze(self, *args, **kwargs): - """American spelling of the analyse method \n""" - __doc__ += self.analyse.__doc__ + """American spelling of the analyse method""" self.analyse(*args, **kwargs) def analyse_and_extract(self, path=None, *args, **kwargs): @@ -362,16 +358,11 @@ def analyse_and_extract(self, path=None, *args, **kwargs): """ raise NotImplementedError("this function is not available for the selected backend") - #FIXME check the funciton and 'memory only parameter + # FIXME check the funciton and 'memory only parameter def analyse_and_store(self, memory_only=False, *args, **kwargs): """Analyse the problem in the selected backend and stores the results in the model. - Note - ---- - The extraction of the results to SQLite ca be done `in memory` to speed up - the process but no database file is generated. - Parameters ---------- problems : [:class:`compas_fea2.problem.Problem`], optional @@ -380,6 +371,12 @@ def analyse_and_store(self, memory_only=False, *args, **kwargs): memory_only : bool, optional store the SQLITE database only in memory (no .db file will be saved), by default False + + Notes + ----- + The extraction of the results to SQLite ca be done `in memory` to speed up + the process but no database file is generated. + """ self.analyse(*args, **kwargs) self.convert_results_to_sqlite(*args, **kwargs) @@ -389,11 +386,6 @@ def restart_analysis(self, *args, **kwargs): """Continue a previous analysis from a given increement with additional steps. - Note - ---- - For abaqus, you have to specify to save specific files during the original - analysis by passing the `restart=True` option. - Parameters ---------- problem : :class:`compas_fea2.problme.Problem` @@ -407,6 +399,12 @@ def restart_analysis(self, *args, **kwargs): ------ ValueError _description_ + + Notes + ----- + For abaqus, you have to specify to save specific files during the original + analysis by passing the `restart=True` option. + """ raise NotImplementedError("this function is not available for the selected backend") @@ -414,8 +412,7 @@ def restart_analysis(self, *args, **kwargs): # Results methods - general # ========================================================================= - - @timer(message='Problem results copied in the model in ') + @timer(message="Problem results copied in the model in ") def store_results_in_model(self, database_path=None, database_name=None, steps=None, fields=None, *args, **kwargs): """Copy the results form the sqlite database back into the model at the nodal and element level. @@ -428,7 +425,7 @@ def store_results_in_model(self, database_path=None, database_name=None, steps=N name of the database file_format : str, optional serialization type ('pkl' or 'json'), by default 'pkl' - steps : :class:`compas_fea2.problem._Step`, optional + steps : :class:`compas_fea2.problem.Step`, optional The steps fro which copy the results, by default `None` (all the steps are saved) fields : _type_, optional Fields results to save, by default `None` (all available fields are saved) @@ -438,7 +435,9 @@ def store_results_in_model(self, database_path=None, database_name=None, steps=N None """ - databse_full_path = os.path.join(database_path, database_name) if database_path and database_name else self.path_results + databse_full_path = ( + os.path.join(database_path, database_name) if database_path and database_name else self.path_results + ) if not os.path.exists(databse_full_path): self.convert_results_to_sqlite(*args, **kwargs) for step in steps or self.steps: @@ -463,7 +462,7 @@ def get_reaction_forces_sql(self, step=None): """ if not step: step = self._steps_order[-1] - _, col_val = self._get_field_results('RF', step) + _, col_val = self._get_field_results("RF", step) return self._get_vector_results(col_val) def get_reaction_moments_sql(self, step=None): @@ -481,14 +480,14 @@ def get_reaction_moments_sql(self, step=None): """ if not step: step = self._steps_order[-1] - _, col_val = self._get_field_results('RM', step) + _, col_val = self._get_field_results("RM", step) return self._get_vector_results(col_val) # ========================================================================= # Results methods - displacements # ========================================================================= - # TODO add moments + # TODO add moments def get_total_reaction(self): reactions_forces = [] for part in self.step.problem.model.parts: @@ -507,8 +506,9 @@ def get_total_moment(self): def get_deformed_model(self, step=None, **kwargs): from copy import deepcopy + if not step: - step=self.steps_order[-1] + step = self.steps_order[-1] deformed_model = deepcopy(self.model) # # # TODO create a copy of the model first @@ -520,10 +520,10 @@ def get_deformed_model(self, step=None, **kwargs): raise NotImplementedError() return deformed_model - # ========================================================================= # Viewer methods # ========================================================================= + # def show(self, scale_factor=1., step=None, width=1600, height=900, parts=None, # solid=True, draw_nodes=False, node_labels=False, # draw_bcs=1., draw_constraints=True, draw_loads=True, **kwargs): @@ -562,18 +562,20 @@ def get_deformed_model(self, step=None, **kwargs): # v.draw_loads(step, scale_factor=kwargs['draw_loads']) # v.show() - def show_nodes_field_vector(self, field_name, vector_sf=1., model_sf=1., step=None, width=1600, height=900, **kwargs): + def show_nodes_field_vector( + self, field_name, vector_sf=1.0, model_sf=1.0, step=None, width=1600, height=900, **kwargs + ): from compas_fea2.UI.viewer import FEA2Viewer - from compas.colors import ColorMap, Color - cmap = kwargs.get('cmap', ColorMap.from_palette('hawaii')) - #ColorMap.from_color(Color.red(), rangetype='light') #ColorMap.from_mpl('viridis') + from compas.colors import ColorMap + + cmap = kwargs.get("cmap", ColorMap.from_palette("hawaii")) # Get values if not step: step = self._steps_order[-1] field = NodeFieldResults(field_name, step) - min_value = field._min_invariants['magnitude'].invariants["MIN(magnitude)"] - max_value = field._max_invariants['magnitude'].invariants["MAX(magnitude)"] + min_value = field._min_invariants["magnitude"].invariants["MIN(magnitude)"] + max_value = field._max_invariants["magnitude"].invariants["MAX(magnitude)"] # Color the mesh pts, vectors, colors = [], [], [] @@ -582,19 +584,19 @@ def show_nodes_field_vector(self, field_name, vector_sf=1., model_sf=1., step=No continue vectors.append(r.vector.scaled(vector_sf)) pts.append(r.location.xyz) - colors.append(cmap(r.invariants['magnitude'], minval=min_value, maxval=max_value)) + colors.append(cmap(r.invariants["magnitude"], minval=min_value, maxval=max_value)) # Display results v = FEA2Viewer(width, height, scale_factor=model_sf) v.draw_nodes_vector(pts=pts, vectors=vectors, colors=colors) v.draw_parts(self.model.parts) - if kwargs.get('draw_bcs', None): - v.draw_bcs(self.model, scale_factor=kwargs['draw_bcs']) - if kwargs.get('draw_loads', None): - v.draw_loads(step, scale_factor=kwargs['draw_loads']) + if kwargs.get("draw_bcs", None): + v.draw_bcs(self.model, scale_factor=kwargs["draw_bcs"]) + if kwargs.get("draw_loads", None): + v.draw_loads(step, scale_factor=kwargs["draw_loads"]) v.show() - def show_nodes_field(self, field_name, component, step=None, width=1600, height=900, model_sf=1., **kwargs): + def show_nodes_field(self, field_name, component, step=None, width=1600, height=900, model_sf=1.0, **kwargs): """Display a contour plot of a given field and component. The field must de defined at the nodes of the model (e.g displacement field). @@ -632,65 +634,69 @@ def show_nodes_field(self, field_name, component, step=None, width=1600, height= ------ ValueError _description_ + """ from compas_fea2.UI.viewer import FEA2Viewer from compas.colors import ColorMap, Color - cmap = kwargs.get('cmap', ColorMap.from_palette('hawaii')) - #ColorMap.from_color(Color.red(), rangetype='light') #ColorMap.from_mpl('viridis') + + cmap = kwargs.get("cmap", ColorMap.from_palette("hawaii")) + # ColorMap.from_color(Color.red(), rangetype='light') #ColorMap.from_mpl('viridis') # Get mesh - parts_gkey_vertex={} - parts_mesh={} + parts_gkey_vertex = {} + parts_mesh = {} for part in self.model.parts: - if (mesh:= part.discretized_boundary_mesh): + if mesh := part.discretized_boundary_mesh: colored_mesh = mesh.copy() parts_gkey_vertex[part.name] = colored_mesh.gkey_key(compas_fea2.PRECISION) parts_mesh[part.name] = colored_mesh else: - raise AttributeError('Discretized boundary mesh not found') + raise AttributeError("Discretized boundary mesh not found") # Set the bounding limits - if kwargs.get('bound', None): - if not isinstance(kwargs['bound'], Iterable) or len(kwargs['bound'])!=2: - raise ValueError('You need to provide an upper and lower bound -> (lb, up)') - if kwargs['bound'][0]>kwargs['bound'][1]: - kwargs['bound'][0], kwargs['bound'][1] = kwargs['bound'][1], kwargs['bound'][0] + if kwargs.get("bound", None): + if not isinstance(kwargs["bound"], Iterable) or len(kwargs["bound"]) != 2: + raise ValueError("You need to provide an upper and lower bound -> (lb, up)") + if kwargs["bound"][0] > kwargs["bound"][1]: + kwargs["bound"][0], kwargs["bound"][1] = kwargs["bound"][1], kwargs["bound"][0] # Get values if not step: step = self._steps_order[-1] field = NodeFieldResults(field_name, step) - min_value = field._min_components[component].components[f'MIN({component})'] - max_value = field._max_components[component].components[f'MAX({component})'] + min_value = field._min_components[component].components[f"MIN({component})"] + max_value = field._max_components[component].components[f"MAX({component})"] # Color the mesh for r in field.results: - if min_value - max_value == 0.: + if min_value - max_value == 0.0: color = Color.red() - elif kwargs.get('bound', None): - if r.components[component]>=kwargs['bound'] or r.components[component]<=kwargs['bound']: + elif kwargs.get("bound", None): + if r.components[component] >= kwargs["bound"] or r.components[component] <= kwargs["bound"]: color = Color.red() else: color = cmap(r.components[component], minval=min_value, maxval=max_value) else: color = cmap(r.components[component], minval=min_value, maxval=max_value) if r.location.gkey in parts_gkey_vertex[part.name]: - parts_mesh[part.name].vertex_attribute(parts_gkey_vertex[part.name][r.location.gkey], 'color', color) + parts_mesh[part.name].vertex_attribute(parts_gkey_vertex[part.name][r.location.gkey], "color", color) # Display results v = FEA2Viewer(width, height, scale_factor=model_sf) for part in self.model.parts: v.draw_mesh(parts_mesh[part.name]) - if kwargs.get('draw_bcs', None): - v.draw_bcs(self.model, scale_factor=kwargs['draw_bcs']) + if kwargs.get("draw_bcs", None): + v.draw_bcs(self.model, scale_factor=kwargs["draw_bcs"]) - if kwargs.get('draw_loads', None): - v.draw_loads(step, scale_factor=kwargs['draw_loads']) + if kwargs.get("draw_loads", None): + v.draw_loads(step, scale_factor=kwargs["draw_loads"]) v.show() - def show_displacements(self, component=3, step=None, style='contour', deformed=False, width=1600, height=900, model_sf=1., **kwargs): + def show_displacements( + self, component=3, step=None, style="contour", deformed=False, width=1600, height=900, model_sf=1.0, **kwargs + ): """Display the displacement of the nodes. Parameters @@ -723,21 +729,30 @@ def show_displacements(self, component=3, step=None, style='contour', deformed=F Raises ------ ValueError - "The style can be either 'vector' or 'contour'" + The style can be either 'vector' or 'contour'. + """ - if style == 'contour': - self.show_nodes_field(field_name='U', component='U'+str(component), step=step, width=width, height=height, model_sf=model_sf, **kwargs) - elif style == 'vector': - raise NotImplementedError('WIP') + if style == "contour": + self.show_nodes_field( + field_name="U", + component="U" + str(component), + step=step, + width=width, + height=height, + model_sf=model_sf, + **kwargs, + ) + elif style == "vector": + raise NotImplementedError("WIP") else: raise ValueError("The style can be either 'vector' or 'contour'") - def show_deformed(self, step=None, width=1600, height=900, scale_factor=1., **kwargs): + def show_deformed(self, step=None, width=1600, height=900, scale_factor=1.0, **kwargs): """Display the structure in its deformed configuration. Parameters ---------- - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The Step of the analysis, by default None. If not provided, the last step is used. width : int, optional @@ -745,27 +760,27 @@ def show_deformed(self, step=None, width=1600, height=900, scale_factor=1., **kw height : int, optional Height of the viewer window, by default 900 - Return - ------ + Returns + ------- None + """ from compas_fea2.UI.viewer import FEA2Viewer - from compas.geometry import Point, Vector + from compas.geometry import Vector - from compas.colors import ColorMap, Color v = FEA2Viewer(width, height) if not step: - step=self.steps_order[-1] + step = self.steps_order[-1] # TODO create a copy of the model first - displacements = NodeFieldResults('U', step) + displacements = NodeFieldResults("U", step) for displacement in displacements.results: vector = displacement.vector.scaled(scale_factor) displacement.location.xyz = sum_vectors([Vector(*displacement.location.xyz), vector]) v.draw_parts(self.model.parts, solid=True) - if kwargs.get('draw_bcs', None): - v.draw_bcs(self.model, scale_factor=kwargs['draw_bcs']) + if kwargs.get("draw_bcs", None): + v.draw_bcs(self.model, scale_factor=kwargs["draw_bcs"]) - if kwargs.get('draw_loads', None): - v.draw_loads(step, scale_factor=kwargs['draw_loads']) + if kwargs.get("draw_loads", None): + v.draw_loads(step, scale_factor=kwargs["draw_loads"]) v.show() diff --git a/src/compas_fea2/problem/steps/__init__.py b/src/compas_fea2/problem/steps/__init__.py index 1e6ba537b..e69de29bb 100644 --- a/src/compas_fea2/problem/steps/__init__.py +++ b/src/compas_fea2/problem/steps/__init__.py @@ -1,32 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from .step import ( - _Step, - _GeneralStep, -) - -from .static import ( - StaticStep, - StaticRiksStep, -) - -from .dynamic import ( - DynamicStep, -) - -from .quasistatic import ( - QuasiStaticStep, - DirectCyclicStep, -) - -from .perturbations import ( - _Perturbation, - ModalAnalysis, - ComplexEigenValue, - BucklingAnalysis, - LinearStaticPerturbation, - StedyStateDynamic, - SubstructureGeneration, -) diff --git a/src/compas_fea2/problem/steps/dynamic.py b/src/compas_fea2/problem/steps/dynamic.py index ff8331d8d..b9195e076 100644 --- a/src/compas_fea2/problem/steps/dynamic.py +++ b/src/compas_fea2/problem/steps/dynamic.py @@ -2,22 +2,11 @@ from __future__ import division from __future__ import print_function -from compas_fea2.base import FEAData -from .step import _Step +from .step import Step -class DynamicStep(_Step): - """Step for dynamic analysis. - - Parameters - ---------- - None - - Attributes - ---------- - None - - """ +class DynamicStep(Step): + """Step for dynamic analysis.""" def __init__(self, name=None, **kwargs): super(DynamicStep, self).__init__(name=name, **kwargs) diff --git a/src/compas_fea2/problem/steps/perturbations.py b/src/compas_fea2/problem/steps/perturbations.py index 0f7ab8131..80ec97db3 100644 --- a/src/compas_fea2/problem/steps/perturbations.py +++ b/src/compas_fea2/problem/steps/perturbations.py @@ -2,26 +2,25 @@ from __future__ import division from __future__ import print_function -from compas_fea2.base import FEAData -from .step import _Step +from .step import Step -class _Perturbation(_Step): +class Perturbation(Step): """A perturbation is a change of the state of the structure after an analysis step. Differently from Steps, perturbations' changes are not carried over to the next step. Parameters ---------- - _Step : _type_ + Step : _type_ _description_ """ def __init__(self, name=None, **kwargs): - super(_Perturbation, self).__init__(name=name, **kwargs) + super(Perturbation, self).__init__(name=name, **kwargs) -class ModalAnalysis(_Perturbation): +class ModalAnalysis(Perturbation): """Perform a modal analysis of the Model from the resulting state after an analysis Step. @@ -31,6 +30,7 @@ class ModalAnalysis(_Perturbation): Name of the ModalStep. modes : int Number of modes to analyse. + """ def __init__(self, modes=1, name=None, **kwargs): @@ -38,7 +38,7 @@ def __init__(self, modes=1, name=None, **kwargs): self.modes = modes -class ComplexEigenValue(_Perturbation): +class ComplexEigenValue(Perturbation): """""" def __init__(self, name=None, **kwargs): @@ -46,7 +46,7 @@ def __init__(self, name=None, **kwargs): raise NotImplementedError -class BucklingAnalysis(_Perturbation): +class BucklingAnalysis(Perturbation): """""" def __init__(self, modes, vectors=None, iterations=30, algorithm=None, name=None, **kwargs): @@ -57,20 +57,20 @@ def __init__(self, modes, vectors=None, iterations=30, algorithm=None, name=None self._algorithm = algorithm def _compute_vectors(self, modes): - self._vectors = modes*2 + self._vectors = modes * 2 if modes > 9: self._vectors += modes @staticmethod def Lanczos(modes, name=None): - return BucklingAnalysis(modes=modes, vectors=None, algorithhm='Lanczos', name=name) + return BucklingAnalysis(modes=modes, vectors=None, algorithhm="Lanczos", name=name) @staticmethod def Subspace(modes, iterations, vectors=None, name=None): - return BucklingAnalysis(modes=modes, vectors=vectors, iterations=iterations, algorithhm='Subspace', name=name) + return BucklingAnalysis(modes=modes, vectors=vectors, iterations=iterations, algorithhm="Subspace", name=name) -class LinearStaticPerturbation(_Perturbation): +class LinearStaticPerturbation(Perturbation): """""" def __init__(self, name=None, **kwargs): @@ -78,7 +78,7 @@ def __init__(self, name=None, **kwargs): raise NotImplementedError -class StedyStateDynamic(_Perturbation): +class StedyStateDynamic(Perturbation): """""" def __init__(self, name=None, **kwargs): @@ -86,7 +86,7 @@ def __init__(self, name=None, **kwargs): raise NotImplementedError -class SubstructureGeneration(_Perturbation): +class SubstructureGeneration(Perturbation): """""" def __init__(self, name=None, **kwargs): diff --git a/src/compas_fea2/problem/steps/quasistatic.py b/src/compas_fea2/problem/steps/quasistatic.py index 7855b9b5f..19ccf1158 100644 --- a/src/compas_fea2/problem/steps/quasistatic.py +++ b/src/compas_fea2/problem/steps/quasistatic.py @@ -2,40 +2,19 @@ from __future__ import division from __future__ import print_function -from compas_fea2.base import FEAData -from .step import _Step +from .step import Step -class QuasiStaticStep(_Step): - """Step for quasi-static analysis. - - Parameters - ---------- - None - - Attributes - ---------- - None - - """ +class QuasiStaticStep(Step): + """Step for quasi-static analysis.""" def __init__(self, name=None, **kwargs): super(QuasiStaticStep, self).__init__(name=name, **kwargs) raise NotImplementedError -class DirectCyclicStep(_Step): - """Step for a direct cyclic analysis. - - Parameters - ---------- - None - - Attributes - ---------- - None - - """ +class DirectCyclicStep(Step): + """Step for a direct cyclic analysis.""" def __init__(self, name=None, **kwargs): super(DirectCyclicStep, self).__init__(name=name, **kwargs) diff --git a/src/compas_fea2/problem/steps/static.py b/src/compas_fea2/problem/steps/static.py index 4f7c72191..598524f83 100644 --- a/src/compas_fea2/problem/steps/static.py +++ b/src/compas_fea2/problem/steps/static.py @@ -2,24 +2,18 @@ from __future__ import division from __future__ import print_function from typing import Iterable -from xml.dom.minidom import Element from compas_fea2.model.nodes import Node -from compas_fea2.model.groups import ElementsGroup, PartsGroup - - -from compas_fea2.problem.loads import _Load from compas_fea2.problem.loads import GravityLoad from compas_fea2.problem.loads import PointLoad - from compas_fea2.problem.patterns import Pattern from compas_fea2.problem.displacements import GeneralDisplacement from compas_fea2.problem.fields import PrescribedTemperatureField -from .step import _GeneralStep +from .step import GeneralStep -class StaticStep(_GeneralStep): +class StaticStep(GeneralStep): """StaticStep for use in a static analysis. Parameters @@ -78,21 +72,47 @@ class StaticStep(_GeneralStep): Gravity load to assing to the whole model. displacements : dict Dictionary of the displacements assigned to each part in the model in the step. - """ - def __init__(self, max_increments=100, initial_inc_size=1, min_inc_size=0.00001, time=1, nlgeom=False, modify=True, name=None, **kwargs): - super(StaticStep, self).__init__(max_increments=max_increments, - initial_inc_size=initial_inc_size, min_inc_size=min_inc_size, - time=time, nlgeom=nlgeom, modify=modify, name=name, **kwargs) + """ - def add_node_load(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): + def __init__( + self, + max_increments=100, + initial_inc_size=1, + min_inc_size=0.00001, + time=1, + nlgeom=False, + modify=True, + name=None, + **kwargs, + ): + super(StaticStep, self).__init__( + max_increments=max_increments, + initial_inc_size=initial_inc_size, + min_inc_size=min_inc_size, + time=time, + nlgeom=nlgeom, + modify=modify, + name=name, + **kwargs, + ) + + def add_node_load( + self, + nodes, + x=None, + y=None, + z=None, + xx=None, + yy=None, + zz=None, + axes="global", + name=None, + **kwargs, + ): """Add a :class:`compas_fea2.problem.PointLoad` subclass object to the ``Step`` at specific Nodes. - Warning - ------- - local axes are not supported yet - Parameters ---------- name : str @@ -112,22 +132,38 @@ def add_node_load(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=None axes : str, optional 'local' or 'global' axes, by default 'global' - Return - ------ + Returns + ------- :class:`compas_fea2.problem.PointLoad` - """ - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') - return self._add_pattern(Pattern(value=PointLoad(x, y, z, xx, yy, zz, axes, name, **kwargs), distribution=nodes)) - def add_point_load(self, points, tolerance=1000, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): + Warnings + -------- + local axes are not supported yet + + """ + if axes != "global": + raise NotImplementedError("local axes are not supported yet") + return self._add_pattern( + Pattern(value=PointLoad(x, y, z, xx, yy, zz, axes, name, **kwargs), distribution=nodes) + ) + + def add_point_load( + self, + points, + tolerance=1000, + x=None, + y=None, + z=None, + xx=None, + yy=None, + zz=None, + axes="global", + name=None, + **kwargs, + ): """Add a :class:`compas_fea2.problem.PointLoad` subclass object to the ``Step`` at specific points. - Warning - ------- - local axes are not supported yet - Parameters ---------- name : str @@ -147,33 +183,27 @@ def add_point_load(self, points, tolerance=1000, x=None, y=None, z=None, xx=None axes : str, optional 'local' or 'global' axes, by default 'global' - Return - ------ + Returns + ------- :class:`compas_fea2.problem.PointLoad` + + Warnings + -------- + local axes are not supported yet + """ - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') + if axes != "global": + raise NotImplementedError("local axes are not supported yet") if not self.problem: - raise ValueError('No problem assigned to the step.') + raise ValueError("No problem assigned to the step.") if not self.model: - raise ValueError('No model assigned to the problem.') + raise ValueError("No model assigned to the problem.") nodes = [self.model.find_closest_nodes_to_point(point, distance=tolerance)[0][0] for point in points] self.add_node_load(nodes, x=x, y=y, z=z, xx=xx, yy=yy, zz=zz, axes=axes, name=name, **kwargs) - def add_gravity_load(self, g=9.81, x=0., y=0., z=-1.): + def add_gravity_load(self, g=9.81, x=0.0, y=0.0, z=-1.0): """Add a :class:`compas_fea2.problem.GravityLoad` load to the ``Step`` - Note - ---- - The gravity field is applied to the whole model. To remove parts of the - model from the calculation of the gravity force, you can assign to them - a 0 mass material. - - Warning - ------- - Be careful to assign a value of *g* consistent with the units in your - model! - Parameters ---------- g : float, optional @@ -186,6 +216,18 @@ def add_gravity_load(self, g=9.81, x=0., y=0., z=-1.): z component of the gravity direction vector (in global coordinates), by default -1. distribution : [:class:`compas_fea2.model.PartsGroup`] | [:class:`compas_fea2.model.ElementsGroup`] Group of parts or elements affected by gravity. + + Notes + ----- + The gravity field is applied to the whole model. To remove parts of the + model from the calculation of the gravity force, you can assign to them + a 0 mass material. + + Warnings + -------- + Be careful to assign a value of *g* consistent with the units in your + model! + """ # TODO implement distribution # if isinstance(distribution, PartsGroup): @@ -199,14 +241,24 @@ def add_gravity_load(self, g=9.81, x=0., y=0., z=-1.): def add_prestress_load(self): raise NotImplementedError - def add_line_load(self, polyline, discretization=10, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', distance=500, name=None, **kwargs): + def add_line_load( + self, + polyline, + discretization=10, + x=None, + y=None, + z=None, + xx=None, + yy=None, + zz=None, + axes="global", + distance=500, + name=None, + **kwargs, + ): """Add a :class:`compas_fea2.problem.PointLoad` subclass object to the ``Step`` along a prescribed path. - Warning - ------- - local axes are not supported yet - Parameters ---------- name : str @@ -231,44 +283,62 @@ def add_line_load(self, polyline, discretization=10, x=None, y=None, z=None, xx= axes : str, optional 'local' or 'global' axes, by default 'global' - Return - ------ + Returns + ------- :class:`compas_fea2.problem.PointLoad` + + Warnings + -------- + local axes are not supported yet + """ - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') + if axes != "global": + raise NotImplementedError("local axes are not supported yet") if not self.problem: - raise ValueError('No problem assigned to the step.') + raise ValueError("No problem assigned to the step.") if not self.model: - raise ValueError('No model assigned to the problem.') - nodes = [self.model.find_closest_nodes_to_point(point, distance=distance)[0][0] for point in polyline.divide_polyline(discretization)] + raise ValueError("No model assigned to the problem.") + nodes = [ + self.model.find_closest_nodes_to_point(point, distance=distance)[0][0] + for point in polyline.divide_polyline(discretization) + ] n_nodes = len(nodes) - self.add_node_load(nodes, - x=x/n_nodes if x else x, - y=y/n_nodes if y else y, - z=z/n_nodes if z else z, - xx=xx/n_nodes if xx else xx, - yy=yy/n_nodes if yy else yy, - zz=zz/n_nodes if zz else zz, - axes=axes, name=name, **kwargs) - - def add_planar_area_load(self, polygon, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') + self.add_node_load( + nodes, + x=x / n_nodes if x else x, + y=y / n_nodes if y else y, + z=z / n_nodes if z else z, + xx=xx / n_nodes if xx else xx, + yy=yy / n_nodes if yy else yy, + zz=zz / n_nodes if zz else zz, + axes=axes, + name=name, + **kwargs, + ) + + def add_planar_area_load( + self, polygon, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", name=None, **kwargs + ): + if axes != "global": + raise NotImplementedError("local axes are not supported yet") if not self.problem: - raise ValueError('No problem assigned to the step.') + raise ValueError("No problem assigned to the step.") if not self.model: - raise ValueError('No model assigned to the problem.') + raise ValueError("No model assigned to the problem.") nodes = self.model.find_nodes_in_polygon(polygon)[0] n_nodes = len(nodes) - self.add_node_load(nodes, - x=x/n_nodes if x else x, - y=y/n_nodes if y else y, - z=z/n_nodes if z else z, - xx=xx/n_nodes if xx else xx, - yy=yy/n_nodes if yy else yy, - zz=zz/n_nodes if zz else zz, - axes=axes, name=name, **kwargs) + self.add_node_load( + nodes, + x=x / n_nodes if x else x, + y=y / n_nodes if y else y, + z=z / n_nodes if z else z, + xx=xx / n_nodes if xx else xx, + yy=yy / n_nodes if yy else yy, + zz=zz / n_nodes if zz else zz, + axes=axes, + name=name, + **kwargs, + ) def add_tributary_load(self): raise NotImplementedError @@ -279,10 +349,10 @@ def add_tributary_load(self): # FIXME change to pattern def add_temperature_field(self, field, node): if not isinstance(field, PrescribedTemperatureField): - raise TypeError('{!r} is not a PrescribedTemperatureField.'.format(field)) + raise TypeError("{!r} is not a PrescribedTemperatureField.".format(field)) if not isinstance(node, Node): - raise TypeError('{!r} is not a Node.'.format(node)) + raise TypeError("{!r} is not a Node.".format(node)) node._temperature = field self._fields.setdefault(node.part, {}).setdefault(field, set()).add(node) @@ -294,7 +364,9 @@ def add_temperature_fields(self, field, nodes): # ========================================================================= # Displacements methods # ========================================================================= - def add_displacement(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): + def add_displacement( + self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", name=None, **kwargs + ): """Add a displacement at give nodes to the Step object. Parameters @@ -305,9 +377,10 @@ def add_displacement(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=N Returns ------- None + """ - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') + if axes != "global": + raise NotImplementedError("local axes are not supported yet") displacement = GeneralDisplacement(x=x, y=y, z=z, xx=xx, yy=yy, zz=zz, axes=axes, name=name, **kwargs) if not isinstance(nodes, Iterable): nodes = [nodes] @@ -315,18 +388,18 @@ def add_displacement(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=N class StaticRiksStep(StaticStep): - """Step for use in a static analysis when Riks method is necessary. - - Parameters - ---------- - None - - Attributes - ---------- - None - - """ - - def __init__(self, max_increments=100, initial_inc_size=1, min_inc_size=0.00001, time=1, nlgeom=False, modify=True, name=None, **kwargs): + """Step for use in a static analysis when Riks method is necessary.""" + + def __init__( + self, + max_increments=100, + initial_inc_size=1, + min_inc_size=0.00001, + time=1, + nlgeom=False, + modify=True, + name=None, + **kwargs, + ): super().__init__(max_increments, initial_inc_size, min_inc_size, time, nlgeom, modify, name, **kwargs) raise NotImplementedError diff --git a/src/compas_fea2/problem/steps/step.py b/src/compas_fea2/problem/steps/step.py index e745f5bba..ae12f129d 100644 --- a/src/compas_fea2/problem/steps/step.py +++ b/src/compas_fea2/problem/steps/step.py @@ -1,34 +1,16 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function -from copy import deepcopy -from importlib.metadata import metadata -from json import load -from typing import Type -import compas_fea2 from compas_fea2.base import FEAData -from compas_fea2.model.nodes import Node -from compas_fea2.model.elements import _Element - -from compas_fea2.problem.loads import _Load -from compas_fea2.problem.loads import GravityLoad -from compas_fea2.problem.loads import PointLoad - +from compas_fea2.problem.loads import Load from compas_fea2.problem.patterns import Pattern - from compas_fea2.problem.displacements import GeneralDisplacement - -from compas_fea2.problem.fields import _PrescribedField -from compas_fea2.problem.fields import PrescribedTemperatureField - -from compas_fea2.problem.outputs import _Output +from compas_fea2.problem.fields import PrescribedField from compas_fea2.problem.outputs import FieldOutput from compas_fea2.problem.outputs import HistoryOutput -from compas.geometry import Vector -from compas.geometry import sum_vectors from compas_fea2.utilities._utils import timer import copy @@ -38,21 +20,9 @@ # ============================================================================== -class _Step(FEAData): +class Step(FEAData): """Initialises base Step object. - Note - ---- - Stpes are registered to a :class:`compas_fea2.problem.Problem`. - - Note - ---- - A ``compas_fea2`` analysis is based on the concept of ``steps``, - which represent the sequence in which the state of the model is modified. - Steps can be introduced for example to change the output requests or to change - loads, boundary conditions, analysis procedure, etc. There is no limit on the - number of steps in an analysis. - Parameters ---------- name : str, optional @@ -71,10 +41,20 @@ class _Step(FEAData): results : :class:`compas_fea2.results.StepResults` The results of the analysis at this step + Notes + ----- + Steps are registered to a :class:`compas_fea2.problem.Problem`. + + A ``compas_fea2`` analysis is based on the concept of ``steps``, + which represent the sequence in which the state of the model is modified. + Steps can be introduced for example to change the output requests or to change + loads, boundary conditions, analysis procedure, etc. There is no limit on the + number of steps in an analysis. + """ def __init__(self, name=None, **kwargs): - super(_Step, self).__init__(name=name, **kwargs) + super(Step, self).__init__(name=name, **kwargs) self._field_outputs = set() self._history_outputs = set() self._results = None @@ -109,18 +89,19 @@ def add_output(self, output): Parameters ---------- - output : :class:`compas_fea2.problem._Output` + output : :class:`compas_fea2.problem.Output` The requested output. Returns ------- - :class:`compas_fea2.problem._Output` + :class:`compas_fea2.problem.Output` The requested output. Raises ------ TypeError - if the output is not an instance of an :class:`compas_fea2.problem._Output`. + if the output is not an instance of an :class:`compas_fea2.problem.Output`. + """ output._registration = self if isinstance(output, FieldOutput): @@ -128,13 +109,14 @@ def add_output(self, output): elif isinstance(output, HistoryOutput): self._history_outputs.add(output) else: - raise TypeError('{!r} is not an _Output.'.format(output)) + raise TypeError("{!r} is not an Output.".format(output)) return output # ========================================================================== # Results methods # ========================================================================== - @timer(message='Step results copied in the model in ') + + @timer(message="Step results copied in the model in ") def _store_results_in_model(self, fields=None): """Copy the results for the step in the model object at the nodal and element level. @@ -149,53 +131,54 @@ def _store_results_in_model(self, fields=None): None """ - from compas_fea2.results.sql_wrapper import create_connection_sqlite3, get_database_table, get_all_field_results - import sqlalchemy as db - - engine, connection, metadata = create_connection_sqlite3(self.problem.path_results) - FIELDS = get_database_table(engine, metadata, 'fiedls') - if not fields: - field_column = FIELDS.query.all() - fields=[field for field in field_column.field] - - for field in fields: - field_table = get_database_table(engine, metadata, field) - _, results = get_all_field_results(engine, metadata, field, field_table) - for row in results: - part = self.problem.model.find_part_by_name(row[0]) - if row[2] == 'NODAL': - node_element = part.find_node_by_key(row[3]) - else: - raise NotImplementedError('elements not supported yet') - - node_element._results.setdefault(self.problem, {})[self] = res_field - - - step_results = results[self.name] - # Get part results - for part_name, part_results in step_results.items(): - # Get node/element results - for result_type, nodes_elements_results in part_results.items(): - if result_type not in ['nodes', 'elements']: - continue - # nodes_elements = getattr(self.model.find_part_by_name(part_name, casefold=True), result_type) - func = getattr(self.model.find_part_by_name(part_name, casefold=True), - 'find_{}_by_key'.format(result_type[:-1])) - # Get field results - for key, res_field in nodes_elements_results.items(): - node_element = func(key) - if not node_element: - continue - if fields and not res_field in fields: - continue - node_element._results.setdefault(self.problem, {})[self] = res_field + raise NotImplementedError + # from compas_fea2.results.sql_wrapper import create_connection_sqlite3, get_database_table, get_all_field_results + + # engine, connection, metadata = create_connection_sqlite3(self.problem.path_results) + # FIELDS = get_database_table(engine, metadata, "fields") + # if not fields: + # field_column = FIELDS.query.all() + # fields = [field for field in field_column.field] + + # for field in fields: + # field_table = get_database_table(engine, metadata, field) + # _, results = get_all_field_results(engine, metadata, field, field_table) + # for row in results: + # part = self.problem.model.find_part_by_name(row[0]) + # if row[2] == "NODAL": + # node_element = part.find_node_by_key(row[3]) + # else: + # raise NotImplementedError("elements not supported yet") + + # node_element._results.setdefault(self.problem, {})[self] = res_field + + # step_results = results[self.name] + # # Get part results + # for part_name, part_results in step_results.items(): + # # Get node/element results + # for result_type, nodes_elements_results in part_results.items(): + # if result_type not in ["nodes", "elements"]: + # continue + # # nodes_elements = getattr(self.model.find_part_by_name(part_name, casefold=True), result_type) + # func = getattr( + # self.model.find_part_by_name(part_name, casefold=True), "find_{}_by_key".format(result_type[:-1]) + # ) + # # Get field results + # for key, res_field in nodes_elements_results.items(): + # node_element = func(key) + # if not node_element: + # continue + # if fields and res_field not in fields: + # continue + # node_element._results.setdefault(self.problem, {})[self] = res_field + # ============================================================================== # General Steps # ============================================================================== -class _GeneralStep(_Step): +class GeneralStep(Step): """General Step object for use as a base class in a general static, dynamic or multiphysics analysis. @@ -258,10 +241,22 @@ class _GeneralStep(_Step): Dictionary of the loads assigned to each part in the model in the step. fields : dict Dictionary of the prescribed fields assigned to each part in the model in the step. + """ - def __init__(self, max_increments, initial_inc_size, min_inc_size, time, nlgeom=False, modify=False, restart=False, name=None, **kwargs): - super(_GeneralStep, self).__init__(name=name, **kwargs) + def __init__( + self, + max_increments, + initial_inc_size, + min_inc_size, + time, + nlgeom=False, + modify=False, + restart=False, + name=None, + **kwargs + ): + super(GeneralStep, self).__init__(name=name, **kwargs) self._max_increments = max_increments self._initial_inc_size = initial_inc_size @@ -275,13 +270,13 @@ def __init__(self, max_increments, initial_inc_size, min_inc_size, time, nlgeom= def __rmul__(self, other): if not isinstance(other, (float, int)): - raise TypeError('Step multiplication only allowed with real numbers') + raise TypeError("Step multiplication only allowed with real numbers") step_copy = copy.copy(self) step_copy._patterns = set() for pattern in self._patterns: pattern_copy = copy.copy(pattern) load_copy = copy.copy(pattern.load) - pattern_copy._load = other*load_copy + pattern_copy._load = other * load_copy step_copy._add_pattern(pattern_copy) return step_copy @@ -291,11 +286,11 @@ def displacements(self): @property def loads(self): - return list(filter(lambda p: isinstance(p.load, _Load), self._patterns)) + return list(filter(lambda p: isinstance(p.load, Load), self._patterns)) @property def fields(self): - return list(filter(lambda p: isinstance(p.load, _PrescribedField), self._patterns)) + return list(filter(lambda p: isinstance(p.load, PrescribedField), self._patterns)) @property def max_increments(self): @@ -334,34 +329,34 @@ def restart(self, value): # ========================================================================= def _add_pattern(self, load_pattern): - # type: (_Load, Node | _Element) -> _Load """Add a general load pattern to the Step object. - Warning - ------- - The *load* and the *keys* must be consistent (you should not assing a - line load to a node). Consider using specific methods to assign load, - such as ``add_point_load``, ``add_line_load``, etc. - Parameters ---------- load : obj - any ``compas_fea2`` :class:`compas_fea2.problem._Load` subclass object + any ``compas_fea2`` :class:`compas_fea2.problem.Load` subclass object location : var Location where the load is applied Returns ------- None + + Warnings + -------- + The *load* and the *keys* must be consistent (you should not assing a + line load to a node). Consider using specific methods to assign load, + such as ``add_point_load``, ``add_line_load``, etc. + """ if not isinstance(load_pattern, Pattern): - raise TypeError('{!r} is not a LoadPattern.'.format(load_pattern)) + raise TypeError("{!r} is not a LoadPattern.".format(load_pattern)) if self.problem: if self.model: if not list(load_pattern.distribution).pop().model == self.model: - raise ValueError('The load pattern is not applied to a valid reagion of {!r}'.format(self.model)) + raise ValueError("The load pattern is not applied to a valid reagion of {!r}".format(self.model)) # store location in step self._patterns.add(load_pattern) @@ -370,19 +365,19 @@ def _add_pattern(self, load_pattern): return load_pattern def _add_patterns(self, load_patterns): - # type: (_Load, Node | _Element) -> list(_Load) """Add a load to multiple locations. Parameters ---------- - load : :class:`_Load` + load : :class:`Load` Load to assign to the node location : [var] Locations where the load is applied Returns ------- - load : [:class:`_Load`] + load : [:class:`Load`] Load to assign to the node + """ return [self.add_pattern(load_pattern) for load_pattern in load_patterns] diff --git a/src/compas_fea2/problem/steps_combinations.py b/src/compas_fea2/problem/steps_combinations.py index 82cb3c5f2..6d507e7b1 100644 --- a/src/compas_fea2/problem/steps_combinations.py +++ b/src/compas_fea2/problem/steps_combinations.py @@ -3,25 +3,25 @@ from __future__ import print_function from compas_fea2.base import FEAData -from compas_fea2.problem import Pattern class StepsCombination(FEAData): """A StepsCombination `sums` the analysis results of given steps (:class:`compas_fea2.problem.LoadPattern`). - Note - ---- + Parameters + ---------- + FEAData : _type_ + _description_ + + Notes + ----- By default every analysis in `compas_fea2` is meant to be `non-linear`, in the sense that the effects of a load pattern (:class:`compas_fea2.problem.Pattern`) in a given steps are used as a starting point for the application of the load patterns in the next step. Therefore, the sequence of the steps can affect the results (if the response is actully non-linear). - Parameters - ---------- - FEAData : _type_ - _description_ """ def __init__(self, name=None, **kwargs): diff --git a/src/compas_fea2/results/__init__.py b/src/compas_fea2/results/__init__.py index 4f595642d..64a8a38fe 100644 --- a/src/compas_fea2/results/__init__.py +++ b/src/compas_fea2/results/__init__.py @@ -1,27 +1,12 @@ -""" -******************************************************************************** -results -******************************************************************************** - -.. currentmodule:: compas_fea2.results - -.. autosummary:: - :toctree: generated/ - - Results - NodeFieldResults - -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function from .results import Results, NodeFieldResults -from .sql_wrapper import (create_connection_sqlite3, - get_database_table, - ) -__all__ = [ - 'Results', - 'NodeFieldResults' -] +# from .sql_wrapper import ( +# create_connection_sqlite3, +# get_database_table, +# ) + +__all__ = ["Results", "NodeFieldResults"] diff --git a/src/compas_fea2/results/results.py b/src/compas_fea2/results/results.py index a03ea594e..062019736 100644 --- a/src/compas_fea2/results/results.py +++ b/src/compas_fea2/results/results.py @@ -4,10 +4,8 @@ from typing import Iterable -from compas_fea2.base import FEAData - from compas.geometry import Vector -from compas.geometry import sum_vectors +from compas_fea2.base import FEAData from .sql_wrapper import get_field_results, get_field_labels, get_database_table, create_connection @@ -16,15 +14,16 @@ class Results(FEAData): """Results object. This ensures that the results from all the backends are consistent. - Note - ---- - Results are registered to a :class:`compas_fea2.problem.Problem`. - Parameters ---------- location : var location of the result value : var + + Notes + ----- + Results are registered to a :class:`compas_fea2.problem.Problem`. + """ def __init__(self, location, components, invariants, name=None, **kwargs): @@ -47,29 +46,25 @@ def location(self): @property def vector(self): - if len(self.components)==3: + if len(self.components) == 3: return Vector(*list(self.components.values())) @property def value(self): return self.vector.length - def to_file(self, *args, **kwargs): raise NotImplementedError("this function is not available for the selected backend") + class FieldResults(FEAData): def __init__(self, field_name, step, name=None, *args, **kwargs): super(FieldResults, self).__init__(name, *args, **kwargs) self._registration = step self._db_connection = create_connection(self.problem.path_db) self._field_name = field_name - self._components = get_field_labels(*self.db_connection, - self.field_name, - 'components') - self._invariants = get_field_labels(*self.db_connection, - self.field_name, - 'invariants') + self._components = get_field_labels(*self.db_connection, self.field_name, "components") + self._invariants = get_field_labels(*self.db_connection, self.field_name, "invariants") @property def step(self): @@ -91,7 +86,6 @@ def db_connection(self): def db_connection(self, path_db): self._db_connection = create_connection(path_db) - def _get_field_results(self, field): """_summary_ @@ -135,7 +129,7 @@ class NodeFieldResults(FieldResults): def __init__(self, field_name, step, name=None, *args, **kwargs): super(NodeFieldResults, self).__init__(field_name, step, name, *args, **kwargs) self._results = self._link_field_results_to_model(self._get_field_results(field=self.field_name)[1]) - if len(self.results)!=len(self.model.nodes): + if len(self.results) != len(self.model.nodes): raise ValueError('The requested field is not defined at the nodes. Try "show_elements_field" instead".') self._max_components = {c: self._get_limit("MAX", component=c)[0] for c in self._components} self._min_components = {c: self._get_limit("MIN", component=c)[0] for c in self._components} @@ -160,10 +154,11 @@ def results(self): @property def max(self): - return self._max_invariants['magnitude'][0] + return self._max_invariants["magnitude"][0] + @property def min(self): - return self._min_invariants['magnitude'][0] + return self._min_invariants["magnitude"][0] def _link_field_results_to_model(self, field_results): """Converts the values of the results string to actual nodes of the @@ -197,15 +192,15 @@ def _link_field_results_to_model(self, field_results): print("Part {} not found in model".format(row[0])) continue result = Results( - location=part.find_node_by_key(row[2]), - components={col_names[i]: row[i] for i in range(3, len(self.components)+3)}, - invariants={col_names[i]: row[i] for i in range(len(self.components)+3, len(row))} + location=part.find_node_by_key(row[2]), + components={col_names[i]: row[i] for i in range(3, len(self.components) + 3)}, + invariants={col_names[i]: row[i] for i in range(len(self.components) + 3, len(row))}, ) results.append(result) return results def _get_limit(self, limit="MAX", component="magnitude"): - if component not in self.components+self.invariants: + if component not in self.components + self.invariants: raise ValueError( "The specified component is not valid. Choose from {}".format(self._components + self.invariants) ) @@ -224,10 +219,11 @@ def get_value_at_nodes(self, nodes): steps : _type_, optional _description_, by default None - Return - ------ + Returns + ------- dict Dictionary with {'part':..; 'node':..; 'vector':...} + """ if not isinstance(nodes, Iterable): nodes = [nodes] @@ -264,10 +260,11 @@ def get_value_at_point(self, point, distance, plane=None, steps=None, group_by=[ steps : _type_, optional _description_, by default None - Return - ------ + Returns + ------- dict Dictionary with {'part':..; 'node':..; 'vector':...} + """ steps = [self.step] node = self.model.find_node_by_location(point, distance, plane=None) diff --git a/src/compas_fea2/results/sql_wrapper.py b/src/compas_fea2/results/sql_wrapper.py index 53b6a111c..29c54eefb 100644 --- a/src/compas_fea2/results/sql_wrapper.py +++ b/src/compas_fea2/results/sql_wrapper.py @@ -1,13 +1,10 @@ -from operator import index import sqlalchemy as db - -from math import sqrt -import os import sqlite3 from sqlite3 import Error # TODO convert to sqlalchemy + def create_connection_sqlite3(db_file=None): """Create a database connection to the SQLite database specified by db_file. @@ -17,14 +14,15 @@ def create_connection_sqlite3(db_file=None): Path to the .db file, by default 'None'. If not provided, the database is run in memory. - Return - ------ + Returns + ------- :class:`sqlite3.Connection` | None Connection object or None + """ conn = None try: - conn = sqlite3.connect(db_file or ':memory:') + conn = sqlite3.connect(db_file or ":memory:") except Error as e: print(e) return conn @@ -40,9 +38,10 @@ def _create_table_sqlite3(conn, sql): create_table_sql : str A CREATE TABLE statement - Return - ------ + Returns + ------- None + """ try: c = conn.cursor() @@ -50,6 +49,7 @@ def _create_table_sqlite3(conn, sql): except Error as e: print(e) + def _insert_entry__sqlite3(conn, sql): """General code to insert an entry in a table @@ -60,9 +60,10 @@ def _insert_entry__sqlite3(conn, sql): sql : _type_ _description_ - Return - ------ + Returns + ------- lastrowid + """ try: c = conn.cursor() @@ -73,24 +74,27 @@ def _insert_entry__sqlite3(conn, sql): exit() return c.lastrowid + def create_field_description_table_sqlite3(conn): - """ Create the table containing general results information and field + """Create the table containing general results information and field descriptions. Parameters ---------- conn : - Return - ------ + Returns + ------- None + """ with conn: sql = """CREATE TABLE IF NOT EXISTS fields (field text, description text, components text, invariants text, UNIQUE(field) );""" _create_table_sqlite3(conn, sql) + def insert_field_description_sqlite3(conn, field, description, components_names, invariants_names): - """ Create the table containing general results information and field + """Create the table containing general results information and field descriptions. Parameters @@ -104,18 +108,21 @@ def insert_field_description_sqlite3(conn, field, description, components_names, invariants_names : Iterable Output field invariants names. - Return - ------ + Returns + ------- None + """ - sql = """ INSERT OR IGNORE INTO fields VALUES ('{}', '{}', '{}', '{}')""".format(field, - description, - components_names, - invariants_names, - ) + sql = """ INSERT OR IGNORE INTO fields VALUES ('{}', '{}', '{}', '{}')""".format( + field, + description, + components_names, + invariants_names, + ) return _insert_entry__sqlite3(conn, sql) + def create_field_table_sqlite3(conn, field, components_names): """Create the results table for the given field. @@ -130,14 +137,16 @@ def create_field_table_sqlite3(conn, field, components_names): invariants_names : Iterable Output field invariants names. - Return - ------ + Returns + ------- None + """ # FOREIGN KEY (step) REFERENCES analysis_results (step_name), with conn: sql = """CREATE TABLE IF NOT EXISTS {} (step text, part text, type text, position text, key integer, {});""".format( - field, ', '.join(['{} float'.format(c) for c in components_names])) + field, ", ".join(["{} float".format(c) for c in components_names]) + ) _create_table_sqlite3(conn, sql) @@ -153,21 +162,17 @@ def insert_field_results_sqlite3(conn, field, node_results_data): node_results_data : Iterable Output field components values. - Return - ------ + Returns + ------- int Index of the inserted item. + """ - sql = """ INSERT INTO {} VALUES ({})""".format(field, - ', '.join( - ["'"+str(c)+"'" for c in node_results_data]) - ) + sql = """ INSERT INTO {} VALUES ({})""".format(field, ", ".join(["'" + str(c) + "'" for c in node_results_data])) return _insert_entry__sqlite3(conn, sql) - - def create_connection(db_file=None): """Create a database connection to the SQLite database specified by db_file. @@ -177,16 +182,18 @@ def create_connection(db_file=None): Path to the .db file, by default 'None'. If not provided, the database is run in memory. - Return - ------ + Returns + ------- :class:`sqlite3.Connection` | None Connection object or None + """ engine = db.create_engine("sqlite:///{}".format(db_file)) connection = engine.connect() metadata = db.MetaData() return engine, connection, metadata + def get_database_table(engine, metadata, table_name): """Retrieve a table from the database. @@ -206,6 +213,7 @@ def get_database_table(engine, metadata, table_name): """ return db.Table(table_name, metadata, autoload=True, autoload_with=engine) + def get_query_results(connection, table, columns, test): """Get the filtering query to execute. @@ -230,6 +238,7 @@ def get_query_results(connection, table, columns, test): ResultSet = ResultProxy.fetchall() return ResultProxy, ResultSet + def get_field_labels(engine, connection, metadata, field, label): """Get the names of the components or invariants of the field @@ -251,40 +260,40 @@ def get_field_labels(engine, connection, metadata, field, label): _type_ _description_ """ - FIELDS = get_database_table(engine, metadata, 'fields') + FIELDS = get_database_table(engine, metadata, "fields") query = db.select([FIELDS.columns[label]]).where(FIELDS.columns.field == field) ResultProxy = connection.execute(query) ResultSet = ResultProxy.fetchall() - return ResultSet[0][0].split(' ') + return ResultSet[0][0].split(" ") + def get_all_field_results(engine, connection, metadata, table): - components = get_field_labels(engine, connection, metadata, str(table), 'components') - invariants = get_field_labels(engine, connection, metadata, str(table), 'invariants') - columns = ['part', 'position', 'key']+components+invariants + components = get_field_labels(engine, connection, metadata, str(table), "components") + invariants = get_field_labels(engine, connection, metadata, str(table), "invariants") + columns = ["part", "position", "key"] + components + invariants query = db.select([table.columns[column] for column in columns]) ResultProxy = connection.execute(query) ResultSet = ResultProxy.fetchall() return ResultProxy, ResultSet + def get_field_results(engine, connection, metadata, table, test): - components = get_field_labels(engine, connection, metadata, str(table), 'components') - invariants = get_field_labels(engine, connection, metadata, str(table), 'invariants') - labels = ['part', 'position', 'key']+components+invariants - ResultProxy, ResultSet = get_query_results(connection, - table, - labels, - test) + components = get_field_labels(engine, connection, metadata, str(table), "components") + invariants = get_field_labels(engine, connection, metadata, str(table), "invariants") + labels = ["part", "position", "key"] + components + invariants + ResultProxy, ResultSet = get_query_results(connection, table, labels, test) return ResultProxy, (labels, ResultSet) -if __name__ == '__main__': - import os +if __name__ == "__main__": from pprint import pprint + engine, connection, metadata = create_connection_sqlite3( - r'C:\Code\myRepos\swissdemo\data\q_5\output\1_0\ULS\ULS-results.db') + r"C:\Code\myRepos\swissdemo\data\q_5\output\1_0\ULS\ULS-results.db" + ) # U = db.Table('U', metadata, autoload=True, autoload_with=engine) - U = get_database_table(engine, metadata, 'U') + U = get_database_table(engine, metadata, "U") # print(RF.columns.keys()) # query = db.select([U]).where(U.columns.key == 0) # query = db.select([U]).where(U.columns.part.in_ == ['BLOCK_0', 'TIE_21']) diff --git a/src/compas_fea2/units/__init__.py b/src/compas_fea2/units/__init__.py index 2957ba85a..ee8fc0be5 100644 --- a/src/compas_fea2/units/__init__.py +++ b/src/compas_fea2/units/__init__.py @@ -1,17 +1,10 @@ -""" -******************************************************************************** -Units -******************************************************************************** - -compas_fe2 can use Pint for units consistency. - -""" - import os from pint import UnitRegistry + HERE = os.path.dirname(__file__) # U.define('@alias pascal = Pa') -def units(system='SI'): - return UnitRegistry(os.path.join(HERE, 'fea2_en.txt'), system=system) + +def units(system="SI"): + return UnitRegistry(os.path.join(HERE, "fea2_en.txt"), system=system) diff --git a/src/compas_fea2/utilities/__init__.py b/src/compas_fea2/utilities/__init__.py index 528baa0c7..e11cace39 100644 --- a/src/compas_fea2/utilities/__init__.py +++ b/src/compas_fea2/utilities/__init__.py @@ -1,32 +1,3 @@ -""" -******************************************************************************** -Utilities -******************************************************************************** - -.. currentmodule:: compas_fea2.utilities - - -Functions -========= - -.. autosummary:: - :toctree: generated/ - - colorbar - combine_all_sets - group_keys_by_attribute - group_keys_by_attributes - identify_ranges - mesh_from_shell_elements - network_order - normalise_data - principal_stresses - process_data - postprocess - plotvoxels - - -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -43,20 +14,20 @@ principal_stresses, process_data, postprocess, - plotvoxels + plotvoxels, ) __all__ = [ - 'colorbar', - 'combine_all_sets', - 'group_keys_by_attribute', - 'group_keys_by_attributes', - 'identify_ranges', - 'mesh_from_shell_elements', - 'network_order', - 'normalise_data', - 'principal_stresses', - 'process_data', - 'postprocess', - 'plotvoxels' + "colorbar", + "combine_all_sets", + "group_keys_by_attribute", + "group_keys_by_attributes", + "identify_ranges", + "mesh_from_shell_elements", + "network_order", + "normalise_data", + "principal_stresses", + "process_data", + "postprocess", + "plotvoxels", ] diff --git a/src/compas_fea2/utilities/_utils.py b/src/compas_fea2/utilities/_utils.py index 764da2f6b..2e643b530 100644 --- a/src/compas_fea2/utilities/_utils.py +++ b/src/compas_fea2/utilities/_utils.py @@ -3,7 +3,6 @@ from __future__ import print_function import os -import inspect from subprocess import Popen from subprocess import PIPE @@ -12,24 +11,24 @@ from compas.geometry import bounding_box from compas.geometry import Point, Box -import importlib import itertools from typing import Iterable - def timer(_func=None, *, message=None): """Print the runtime of the decorated function""" + def decorator_timer(func): @wraps(func) def wrapper_timer(*args, **kwargs): - start_time = perf_counter() # 1 + start_time = perf_counter() # 1 value = func(*args, **kwargs) - end_time = perf_counter() # 2 - run_time = end_time - start_time # 3 - m = message or 'Finished {!r} in'.format(func.__name__) - print('{} {:.4f} secs'.format(m, run_time)) + end_time = perf_counter() # 2 + run_time = end_time - start_time # 3 + m = message or "Finished {!r} in".format(func.__name__) + print("{} {:.4f} secs".format(m, run_time)) return value + return wrapper_timer if _func is None: @@ -92,17 +91,21 @@ def get_docstring(cls): """ Decorator: Append to a function's docstring. """ + def _decorator(func): - func_name = func.__qualname__.split('.')[-1] - doc_parts = getattr(cls, func_name).__doc__.split('Returns') + func_name = func.__qualname__.split(".")[-1] + doc_parts = getattr(cls, func_name).__doc__.split("Returns") note = """ Returns ------- list of {} - """.format(doc_parts[1].split('-------\n')[1]) + """.format( + doc_parts[1].split("-------\n")[1] + ) func.__doc__ = doc_parts[0] + note return func + return _decorator @@ -123,26 +126,27 @@ def part_method(f): @wraps(f) def wrapper(*args, **kwargs): - func_name = f.__qualname__.split('.')[-1] + func_name = f.__qualname__.split(".")[-1] self_obj = args[0] - if kwargs.get('dict_format', None): + if kwargs.get("dict_format", None): return {part: vars for part in self_obj.parts if (vars := getattr(part, func_name)(*args[1::], **kwargs))} else: res = [vars for part in self_obj.parts if (vars := getattr(part, func_name)(*args[1::], **kwargs))] - if kwargs.get('merge', None): + if kwargs.get("merge", None): list(itertools.chain.from_iterable(res)) return res + # func_name = f.__qualname__.split('.')[-1] # wrapper.__doc__ = getattr(DeformablePart, func_name).__doc__.split('Returns')[0] # wrapper.__doc__ += "ciao" -# docs = getattr(DeformablePart, method).__doc__.split('Returns', 1)[0] -# docs += """ -# Returns -# ------- -# {:class:`compas_fea2.model.DeformablePart`: var} -# dictionary with the results of the method per each part in the model. -# """ + # docs = getattr(DeformablePart, method).__doc__.split('Returns', 1)[0] + # docs += """ + # Returns + # ------- + # {:class:`compas_fea2.model.DeformablePart`: var} + # dictionary with the results of the method per each part in the model. + # """ return wrapper @@ -159,15 +163,16 @@ def step_method(f): Returns ------- - {:class:`compas_fea2.problem._Step`: var} + {:class:`compas_fea2.problem.Step`: var} dictionary with the results of the method per each step in the problem. """ @wraps(f) def wrapper(*args, **kwargs): - func_name = f.__qualname__.split('.')[-1] + func_name = f.__qualname__.split(".")[-1] self_obj = args[0] return {step: vars for step in self_obj.steps if (vars := getattr(step, func_name)(*args[1::], **kwargs))} + return wrapper @@ -190,21 +195,22 @@ def problem_method(f): @wraps(f) def wrapper(*args, **kwargs): - func_name = f.__qualname__.split('.')[-1] + func_name = f.__qualname__.split(".")[-1] self_obj = args[0] - problems = kwargs.setdefault('problems', self_obj.problems) + problems = kwargs.setdefault("problems", self_obj.problems) if not problems: - raise ValueError('No problems found in the model') + raise ValueError("No problems found in the model") if not isinstance(problems, Iterable): problems = [problems] vars = {} for problem in problems: if problem.model != self_obj: - raise ValueError('{} is not registered to this model'.format(problem)) - if 'steps' in kwargs: - kwargs.setdefault('steps', self_obj.steps) + raise ValueError("{} is not registered to this model".format(problem)) + if "steps" in kwargs: + kwargs.setdefault("steps", self_obj.steps) var = getattr(problem, func_name)(*args[1::], **kwargs) if var: vars[problem] = vars return vars + return wrapper diff --git a/src/compas_fea2/utilities/functions.py b/src/compas_fea2/utilities/functions.py index acff182dd..b500b3f9e 100644 --- a/src/compas_fea2/utilities/functions.py +++ b/src/compas_fea2/utilities/functions.py @@ -47,18 +47,18 @@ __all__ = [ - 'colorbar', - 'combine_all_sets', - 'group_keys_by_attribute', - 'group_keys_by_attributes', - 'network_order', - 'normalise_data', - 'postprocess', - 'process_data', - 'principal_stresses', - 'plotvoxels', - 'identify_ranges', - 'mesh_from_shell_elements' + "colorbar", + "combine_all_sets", + "group_keys_by_attribute", + "group_keys_by_attributes", + "network_order", + "normalise_data", + "postprocess", + "process_data", + "principal_stresses", + "plotvoxels", + "identify_ranges", + "mesh_from_shell_elements", ] @@ -66,8 +66,9 @@ # General methods # ========================================================================= + def process_data(data, dtype, iptype, nodal, elements, n): - """ Process the raw data. + """Process the raw data. Parameters ---------- @@ -93,18 +94,16 @@ def process_data(data, dtype, iptype, nodal, elements, n): """ - if dtype == 'nodal': - + if dtype == "nodal": vn = array(data)[:, newaxis] ve = None - elif dtype == 'element': - + elif dtype == "element": m = len(elements) lengths = zeros(m, dtype=int64) data_array = zeros((m, 20), dtype=float64) - iptypes = {'max': 0, 'min': 1, 'mean': 2, 'abs': 3} + iptypes = {"max": 0, "min": 1, "mean": 2, "abs": 3} for ekey, item in data.items(): fdata = list(item.values()) @@ -129,32 +128,28 @@ def process_data(data, dtype, iptype, nodal, elements, n): AT = A.transpose() def _process(data_array, lengths, iptype): - m = len(lengths) ve = zeros((m, 1)) for i in range(m): - if iptype == 0: - ve[i] = max(data_array[i, :lengths[i]]) + ve[i] = max(data_array[i, : lengths[i]]) elif iptype == 1: - ve[i] = min(data_array[i, :lengths[i]]) + ve[i] = min(data_array[i, : lengths[i]]) elif iptype == 2: - ve[i] = mean(data_array[i, :lengths[i]]) + ve[i] = mean(data_array[i, : lengths[i]]) elif iptype == 3: - ve[i] = max(abs(data_array[i, :lengths[i]])) + ve[i] = max(abs(data_array[i, : lengths[i]])) return ve def _nodal(rows, cols, nodal, ve, n): - vn = zeros((n, 1)) for i in range(len(rows)): - node = cols[i] element = rows[i] @@ -170,18 +165,18 @@ def _nodal(rows, cols, nodal, ve, n): ve = _process(data_array, lengths, iptypes[iptype]) - if nodal == 'mean': + if nodal == "mean": vsum = asarray(AT.dot(ve)) vn = vsum / sum(AT, 1) else: - vn = _nodal(rows, cols, 0 if nodal == 'max' else 1, ve, n) + vn = _nodal(rows, cols, 0 if nodal == "max" else 1, ve, n) return vn, ve def identify_ranges(data): - """ Identifies continuous interger series from a list and returns a list of ranges. + """Identifies continuous interger series from a list and returns a list of ranges. Parameters ---------- @@ -209,8 +204,8 @@ def identify_ranges(data): return ranges -def colorbar(fsc, input='array', type=255): - """ Creates RGB color information from -1 to 1 scaled values. +def colorbar(fsc, input="array", type=255): + """Creates RGB color information from -1 to 1 scaled values. Parameters ---------- @@ -232,16 +227,14 @@ def colorbar(fsc, input='array', type=255): g = -abs(fsc - 0.25) * 2 + 1.5 b = -(fsc - 0.25) * 2 - if input == 'array': - + if input == "array": rgb = hstack([r, g, b]) rgb[rgb > 1] = 1 rgb[rgb < 0] = 0 return rgb * type - elif input == 'float': - + elif input == "float": r = max([0, min([1, r])]) g = max([0, min([1, g])]) b = max([0, min([1, b])]) @@ -250,7 +243,7 @@ def colorbar(fsc, input='array', type=255): def mesh_from_shell_elements(structure): - """ Returns a Mesh datastructure object from a Structure's ShellElement objects. + """Returns a Mesh datastructure object from a Structure's ShellElement objects. Parameters ---------- @@ -264,7 +257,7 @@ def mesh_from_shell_elements(structure): """ - ekeys = [ekey for ekey in structure.elements if structure.elements[ekey].__name__ == 'ShellElement'] + ekeys = [ekey for ekey in structure.elements if structure.elements[ekey].__name__ == "ShellElement"] nkeys = {nkey for ekey in ekeys for nkey in structure.elements[ekey].nodes} mesh = Mesh() @@ -279,17 +272,14 @@ def mesh_from_shell_elements(structure): def volmesh_from_solid_elements(structure): - raise NotImplementedError def network_from_line_elements(structure): - raise NotImplementedError def _angle(A, B, C): - AB = B - A BC = C - B th = arccos(sum(AB * BC) / (sqrt(sum(AB**2)) * sqrt(sum(BC**2)))) * 180 / pi @@ -297,7 +287,6 @@ def _angle(A, B, C): def _centre(p1, p2, p3): - ax, ay = p1[0], p1[1] bx, by = p2[0], p2[1] cx, cy = p3[0], p3[1] @@ -310,13 +299,13 @@ def _centre(p1, p2, p3): g = 2 * (a * (cy - by) - b * (cx - bx)) centerx = (d * e - b * f) / g centery = (a * f - c * e) / g - r = sqrt((ax - centerx)**2 + (ay - centery)**2) + r = sqrt((ax - centerx) ** 2 + (ay - centery) ** 2) return [centerx, centery, 0], r def combine_all_sets(sets_a, sets_b): - """ Combines two nested lists of node or element sets into the minimum ammount of set combinations. + """Combines two nested lists of node or element sets into the minimum ammount of set combinations. Parameters ---------- @@ -337,12 +326,12 @@ def combine_all_sets(sets_a, sets_b): for j in sets_b: for x in sets_a[i]: if x in sets_b[j]: - comb.setdefault(str(i) + ',' + str(j), []).append(x) + comb.setdefault(str(i) + "," + str(j), []).append(x) return comb -def group_keys_by_attribute(adict, name, tol='3f'): - """ Make group keys by shared attribute values. +def group_keys_by_attribute(adict, name, tol="3f"): + """Make group keys by shared attribute values. Parameters ---------- @@ -364,14 +353,14 @@ def group_keys_by_attribute(adict, name, tol='3f'): for key, item in adict.items(): if name in item: value = item[name] - if type(value) == float: - value = '{0:.{1}}'.format(value, tol) + if isinstance(value, float): + value = "{0:.{1}}".format(value, tol) groups.setdefault(value, []).append(key) return groups -def group_keys_by_attributes(adict, names, tol='3f'): - """ Make group keys by shared values of attributes. +def group_keys_by_attributes(adict, names, tol="3f"): + """Make group keys by shared values of attributes. Parameters ---------- @@ -395,20 +384,20 @@ def group_keys_by_attributes(adict, names, tol='3f'): for name in names: if name in item: value = item[name] - if type(value) == float: - value = '{0:.{1}}'.format(value, tol) + if isinstance(value, float): + value = "{0:.{1}}".format(value, tol) else: value = str(value) else: - value = '-' + value = "-" values.append(value) - vkey = '_'.join(values) + vkey = "_".join(values) groups.setdefault(vkey, []).append(key) return groups def network_order(start, structure, network): - """ Extract node and element orders from a Network for a given start-point. + """Extract node and element orders from a Network for a given start-point. Parameters ---------- @@ -433,7 +422,7 @@ def network_order(start, structure, network): """ gkey_key = network.gkey_key() - start = gkey_key[geometric_key(start, '{0}f'.format(structure.tol))] + start = gkey_key[geometric_key(start, "{0}f".format(structure.tol))] leaves = network.leaves() leaves.remove(start) end = leaves[0] @@ -452,14 +441,14 @@ def network_order(start, structure, network): xyz_sp = structure.node_xyz(sp) xyz_ep = structure.node_xyz(ep) dL = distance_point_point(xyz_sp, xyz_ep) - arclengths.append(length + dL / 2.) + arclengths.append(length + dL / 2.0) length += dL return nodes, elements, arclengths, length def normalise_data(data, cmin, cmax): - """ Normalise a vector of data to between -1 and 1. + """Normalise a vector of data to between -1 and 1. Parameters ---------- @@ -491,7 +480,7 @@ def normalise_data(data, cmin, cmax): def postprocess(nodes, elements, ux, uy, uz, data, dtype, scale, cbar, ctype, iptype, nodal): - """ Post-process data from analysis results for given step and field. + """Post-process data from analysis results for given step and field. Parameters ---------- @@ -547,11 +536,11 @@ def postprocess(nodes, elements, ux, uy, uz, data, dtype, scale, cbar, ctype, ip vn, ve = process_data(data=data, dtype=dtype, iptype=iptype, nodal=nodal, elements=elements, n=len(U)) fscaled, fabs = normalise_data(data=vn, cmin=cbar[0], cmax=cbar[1]) - NodeBases = colorbar(fsc=fscaled, input='array', type=ctype) + NodeBases = colorbar(fsc=fscaled, input="array", type=ctype) - if dtype == 'element': + if dtype == "element": escaled, eabs = normalise_data(data=ve, cmin=cbar[0], cmax=cbar[1]) - ElementBases = colorbar(fsc=escaled, input='array', type=ctype) + ElementBases = colorbar(fsc=escaled, input="array", type=ctype) ElementBases_ = [list(i) for i in list(ElementBases)] else: eabs = 0 @@ -566,7 +555,7 @@ def postprocess(nodes, elements, ux, uy, uz, data, dtype, scale, cbar, ctype, ip def plotvoxels(values, U, vdx, indexing=None): - """ Plot values as voxel data. + """Plot values as voxel data. Parameters ---------- @@ -597,7 +586,7 @@ def plotvoxels(values, U, vdx, indexing=None): # Zm, Ym, Xm = meshgrid(X, Y, Z, indexing='ij') f = abs(asarray(values)) - Am = squeeze(griddata(U, f, (Xm, Ym, Zm), method='linear', fill_value=0)) + Am = squeeze(griddata(U, f, (Xm, Ym, Zm), method="linear", fill_value=0)) Am[isnan(Am)] = 0 # voxels = VtkViewer(data={'voxels': Am}) @@ -608,7 +597,7 @@ def plotvoxels(values, U, vdx, indexing=None): def principal_stresses(data, ptype, scale, rotate): - """ Performs principal stress calculations. + """Performs principal stress calculations. Parameters ---------- @@ -636,11 +625,11 @@ def principal_stresses(data, ptype, scale, rotate): """ - axes = data['axes'] - s11 = data['sxx'] - s22 = data['syy'] - s12 = data['sxy'] - spr = data['s{0}p'.format(ptype)] + axes = data["axes"] + s11 = data["sxx"] + s22 = data["syy"] + s12 = data["sxy"] + spr = data["s{0}p".format(ptype)] ekeys = spr.keys() m = len(ekeys) @@ -660,14 +649,14 @@ def principal_stresses(data, ptype, scale, rotate): try: e11[i, :] = axes[ekey][0] e22[i, :] = axes[ekey][1] - s11_sp1[i] = s11[ekey]['ip1_sp1'] - s22_sp1[i] = s22[ekey]['ip1_sp1'] - s12_sp1[i] = s12[ekey]['ip1_sp1'] - spr_sp1[i] = spr[ekey]['ip1_sp1'] - s11_sp5[i] = s11[ekey]['ip1_sp5'] - s22_sp5[i] = s22[ekey]['ip1_sp5'] - s12_sp5[i] = s12[ekey]['ip1_sp5'] - spr_sp5[i] = spr[ekey]['ip1_sp5'] + s11_sp1[i] = s11[ekey]["ip1_sp1"] + s22_sp1[i] = s22[ekey]["ip1_sp1"] + s12_sp1[i] = s12[ekey]["ip1_sp1"] + spr_sp1[i] = spr[ekey]["ip1_sp1"] + s11_sp5[i] = s11[ekey]["ip1_sp5"] + s22_sp5[i] = s22[ekey]["ip1_sp5"] + s12_sp5[i] = s12[ekey]["ip1_sp5"] + spr_sp5[i] = spr[ekey]["ip1_sp5"] except Exception: pass diff --git a/src/compas_fea2/utilities/loads.py b/src/compas_fea2/utilities/loads.py index 18ba1b11c..6b4f65a7f 100644 --- a/src/compas_fea2/utilities/loads.py +++ b/src/compas_fea2/utilities/loads.py @@ -1,4 +1,4 @@ -def mesh_points_pattern(model, mesh, t=0.05, side='top'): +def mesh_points_pattern(model, mesh, t=0.05, side="top"): """Find all the nodes of a model vertically (z) aligned with the vertices of a given mesh. Parameters @@ -24,12 +24,11 @@ def mesh_points_pattern(model, mesh, t=0.05, side='top'): for vertex in mesh.vertices(): point = mesh.vertex_coordinates(vertex) tributary_area = mesh.vertex_area(vertex) - for part in model.parts: #filter(lambda p: 'block' in p.name, model.parts): - nodes = part.find_nodes_where( - [f'{point[0]-t} <= x <= {point[0]+t}', f'{point[1]-t} <= y <= {point[1]+t}']) + for part in model.parts: # filter(lambda p: 'block' in p.name, model.parts): + nodes = part.find_nodes_where([f"{point[0]-t} <= x <= {point[0]+t}", f"{point[1]-t} <= y <= {point[1]+t}"]) if nodes: - if side == 'top': - pattern.setdefault(vertex, {})['area'] = tributary_area - pattern[vertex].setdefault('nodes', []).append(list(sorted(nodes, key=lambda n: n.z))[-1]) + if side == "top": + pattern.setdefault(vertex, {})["area"] = tributary_area + pattern[vertex].setdefault("nodes", []).append(list(sorted(nodes, key=lambda n: n.z))[-1]) # TODO add additional sides return pattern diff --git a/tasks.py b/tasks.py index e1531bdea..d1bbbc512 100644 --- a/tasks.py +++ b/tasks.py @@ -17,17 +17,14 @@ docs.linkcheck, tests.test, tests.testdocs, - build.build_ghuser_components, + tests.testcodeblocks, build.prepare_changelog, build.clean, build.release, + build.build_ghuser_components, ) ns.configure( { "base_folder": os.path.dirname(__file__), - "ghuser": { - "source_dir": "src/compas_fea2/ghpython/components", - "target_dir": "src/compas_fea2/ghpython/components/ghuser", - }, } ) diff --git a/tests/compas_fea2/backends/PLACEHOLDER b/tests/compas_fea2/backends/PLACEHOLDER deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/compas_fea2/backends/abaqus/test_materials.py b/tests/compas_fea2/backends/abaqus/test_materials.py deleted file mode 100644 index 005c9050b..000000000 --- a/tests/compas_fea2/backends/abaqus/test_materials.py +++ /dev/null @@ -1,173 +0,0 @@ -import pytest -import compas_fea2 - -from compas_fea2.model import Concrete - -from compas_fea2.model.materials import Concrete, ElasticIsotropic, Stiff, UserMaterial - -young_mod = 1000 -poisson_ratio = 0.3 -density = 1.0 -name = "test" -unilateral = ["nc", "nt", "junk"] - -# ============================================================================== -# Tests - Elastic Isotropic -# ============================================================================== - - -def test_ElasticIsotropic_generate_data_1(): - material_input = ElasticIsotropic(name, young_mod, poisson_ratio, density, - unilateral=unilateral[0]) - function_out = material_input._generate_jobdata() - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n" - "*NO COMPRESSION\n").format(name, density, young_mod, poisson_ratio) - assert function_out == expected_out - - -def test_ElasticIsotropic_generate_data_2(): - material_input = ElasticIsotropic(name, young_mod, poisson_ratio, density, - unilateral=unilateral[1]) - function_out = material_input._generate_jobdata() - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n" - "*NO TENSION\n").format(name, density, young_mod, poisson_ratio) - assert function_out == expected_out - - -def test_ElasticIsotropic_generate_data_3(): - # FIXME: don't leave this in the future. It's here only to show an example of a badly designed test. - material_input = ElasticIsotropic(name, young_mod, poisson_ratio, density, - unilateral=unilateral[0]) - function_out = material_input._generate_jobdata() - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n" - "*NO TENSION\n").format(name, density, young_mod, poisson_ratio) - assert function_out != expected_out - - -def test_ElasticIsotropic_generate_data_4(): - with pytest.raises(Exception) as exc_info: - ElasticIsotropic(name, young_mod, poisson_ratio, density, unilateral=unilateral[2]) - - assert exc_info.value.args[0] == ("keyword {} for unilateral parameter not recognised. " - "Please review the documentation").format(unilateral[2]) - - -# ============================================================================== -# Tests - Stiff -# ============================================================================== - -def test_Stiff_generate_data(): - # Get the default values for p and v - init_stiff = Stiff("def", 100) - # My material - material_input = Stiff(name, young_mod) - function_out = material_input._generate_jobdata() - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n").format(name, init_stiff.p, young_mod, init_stiff.v['v']) - assert function_out == expected_out - - -# ============================================================================== -# Tests - Steel -# ============================================================================== - -def test_Steel_generate_data(): - E = 1000.0 - fy = 400.0 - fu = 500.0 - eu = 30.0 - # Done in Steel constructor - ep = eu * 1E-2 - (fy * 1E6) / (E * 1E9) - # My material - material_input = Steel("steely", fy, fu, eu, E, 0.3, 1.0) - function_out = material_input._generate_jobdata() - # Expected - # Materials are units dependent - (E, f