Source code for sigmaepsilon.solid.fourier.loads

# -*- coding: utf-8 -*-
import json
import numpy as np
from numpy import ndarray
from typing import Union, Iterable, Any

from dewloosh.core.tools import (allinkwargs, popfromkwargs, 
                                 float_to_str_sig)
from linkeddeepdict import LinkedDeepDict
from linkeddeepdict.tools.dtk import parsedicts_addr

from .problem import NavierProblem
from .preproc import (rhs_rect_const, rhs_conc_1d, rhs_conc_2d, 
                      rhs_line_const)





[docs]class LoadGroup(LinkedDeepDict): """ A class to handle load groups for Navier's semi-analytic solution of rectangular plates and beams with specific boundary conditions. This class is also the base class of all other load types. See Also -------- :class:`LinkedDeepDict` Examples -------- >>> from sigmaepsilon.solid.fourier import LoadGroup, PointLoad >>> loads = LoadGroup( >>> group1 = LoadGroup( >>> case1 = PointLoad(x=L/3, v=[1.0, 0.0]), >>> case2 = PointLoad(x=L/3, v=[0.0, 1.0]), >>> ), >>> group2 = LoadGroup( >>> case1 = PointLoad(x=2*L/3, v=[1.0, 0.0]), >>> case2 = PointLoad(x=2*L/3, v=[0.0, 1.0]), >>> ), >>> ) Since the LoadGroup is a subclass of LinkedDeepDict, a case is accessible as >>> loads['group1', 'case1'] If you want to protect the object from the accidental creation of nested subdirectories, you can lock the layout by typing >>> loads.lock() """ _typestr_ = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__problem = None @property def problem(self) -> NavierProblem: """ Returns the attached problem. """ if self.is_root(): return self.__problem return self.root().problem @problem.setter def problem(self, value:NavierProblem): """ Sets the attached problem. Parameters ---------- value : NavierProblem The problem the loads are defined for. """ assert self.is_root(), "The problem can only be set on the top-level object." self.__problem = value
[docs] def blocks(self, *args, inclusive:bool=False, blocktype:Any=None, deep:bool=True, **kwargs) -> Iterable['LoadGroup']: """ Returns a generator object that yields all the subgroups. Parameters ---------- inclusive : bool, Optional If True, returns the object the call is made upon. Default is False. blocktype : Any, Optional The type of the load groups to return. Default is None, that returns all types. deep : bool, Optional If True, all deep groups are returned separately. Default is True. Yields ------ LoadGroup """ dtype = LoadGroup if blocktype is None else blocktype return self.containers(self, inclusive=inclusive, dtype=dtype, deep=deep)
[docs] def cases(self, *args, inclusive:bool=True, **kwargs) -> Iterable['LoadGroup']: """ Returns a generator that yields the load cases in the group. Parameters ---------- inclusive : bool, Optional If True, returns the object the call is made upon. Default is True. blocktype : Any, Optional The type of the load groups to return. Default is None, that returns all types. deep : bool, Optional If True, all deep groups are returned separately. Default is True. Yields ------ LoadGroup """ return filter(lambda i: i.__class__._typestr_ is not None, self.blocks(*args, inclusive=inclusive, **kwargs))
@staticmethod def _string_to_dtype_(string: str = None): if string == 'group': return LoadGroup elif string == 'rectangle': return RectangleLoad elif string == 'point': return PointLoad elif string == 'line': return LineLoad else: raise NotImplementedError()
[docs] def dump(self, path:str, *, mode:str='w', indent:int=4): """ Dumps the content of the object to a file. Parameters ---------- path : str The path of the file on your filesystem. mode : str, Optional https://www.programiz.com/python-programming/file-operation indent : int, Optional Governs the level to which members will be pretty-printed. Default is 4. """ with open(path, mode) as f: json.dump(self.to_dict(), f, indent=indent)
[docs] @classmethod def from_json(cls, path: str = None) -> 'LoadGroup': """ Loads a loadgroup from a JSON file. Parameters ---------- path : str The path to a file on your filesystem. Returns ------- LoadGroup """ if path is not None: with open(path, 'r') as f: d = json.load(f) return cls.from_dict(d)
def _encode_(self, *args, **kwargs) -> dict: """ Returns the group as a dictionary. Overwrite this in child implementations. """ res = {} cls = type(self) res = { 'type': cls._typestr_, 'key': self.key, } return res @classmethod def _decode_(cls, d: dict = None, *args, **kwargs): """ Returns a LoadGroup object from a dictionary. Overwrite this in child implementations. Parameters ---------- d : dict, Optional A dictionary. **kwargs : dict, Optional Keyword arguments defining a load group. """ if d is None: d = kwargs kwargs = None if kwargs is not None: d.update(kwargs) clskwargs = { 'key': d.pop('key', None), } clskwargs.update(d) return cls(**clskwargs)
[docs] def to_dict(self) -> dict: """ Returns the group as a dictionary. """ res = self._encode_() for key, value in self.items(): if isinstance(value, LoadGroup): res[key] = value.to_dict() else: res[key] = value return res
[docs] @staticmethod def from_dict(d: dict = None, **kwargs) -> 'LoadGroup': """ Reads a LoadGroup from a dictionary. The keys in the dictionaries must match the parameters of the corresponding load types and a type indicator. """ if 'type' in d: cls = LoadGroup._string_to_dtype_(d['type']) else: cls = LoadGroup res = cls(**d) for addr, value in parsedicts_addr(d, inclusive=True): if len(addr) == 0: continue if 'type' in value: cls = LoadGroup._string_to_dtype_(value['type']) else: cls = LoadGroup value['key'] = addr[-1] res[addr] = cls(**value) return res
def __repr__(self): return 'LoadGroup(%s)' % (dict.__repr__(self))
[docs]class RectangleLoad(LoadGroup): """ A class to handle rectangular loads. Parameters ---------- value: Iterable 1d or 2d iterable of scalars for all 3 degrees of freedom in the order :math:`mx, my, fz`. points: Iterable, Optional The coordinates of the lower-left and upper-right points of the region where the load is applied. Default is None. **kwargs : dict, Optional If the region of application is not specified by the argument 'points', extra keyword arguments are forwarded to :func:`get_coords`. Default is None. """ _typestr_ = 'rectangle' def __init__(self, *args, value:Iterable, points:Iterable=None, **kwargs): if points is not None: self.points = np.array(points, dtype=float) else: self.points = RectangleLoad.get_coords(kwargs) self.value = np.array(value, dtype=float) super().__init__() def _encode_(self, *args, **kwargs) -> dict: res = {} cls = type(self) res = { 'type': cls._typestr_, 'key': self.key, 'region': float_to_str_sig(self.region(), sig=6), 'value': float_to_str_sig(self.value, sig=6), } return res @classmethod def _decode_(cls, d: dict = None, *args, **kwargs): if d is None: d = kwargs kwargs = None if kwargs is not None: d.update(kwargs) points = RectangleLoad.get_coords(d) clskwargs = { 'key': d.pop('key', None), 'points': points, 'value': np.array(d.pop('value'), dtype=float) } clskwargs.update(d) return cls(**clskwargs)
[docs] @staticmethod def get_coords(d: dict = None, *args, **kwargs): """ Returns the bottom-left and upper-right coordinates of the region of the load from several inputs. Parameters ---------- d : dict, Optional A dictionary, which is equivalrent to a parameter set from the other parameters listed here. Default is None. region : Iterable, Optional An iterable of length 4 with values x0, y0, w, and h. Here x0 and y0 are the coordinates of the bottom-left corner, w and h are the width and height of the region. xy : Iterable, Optional The position of the bottom-left corner as an iterable of length 2. w : float, Optional The width of the region. h : float, Optional The height of the region. center : Iterable, Optional The coordinates of the center of the region. Returns ------- numpy.ndarray A 2d float array of coordinates, where the entries of the first and second rows are the coordinates of the lower-left and upper-right points of the region. Examples -------- The following definitions return the same output: >>> from sigmaepsilon.solid.fourier import RectLoad >>> RectLoad.get_coords(region=[2, 3, 0.5, 0.7]) >>> RectLoad.get_coords(xy=[2, 3], w=0.5, h=0.7) >>> RectLoad.get_coords(center=[2.25, 3.35], w=0.5, h=0.7) >>> RectLoad.get_coords(dict(center=[2.25, 3.35], w=0.5, h=0.7)) """ points = None if d is None: d = kwargs try: if 'points' in d: points = np.array(d.pop('points')) elif 'region' in d: x0, y0, w, h = np.array(d.pop('region')) points = np.array([[x0, y0], [x0 + w, y0 + h]]) elif allinkwargs(['xy', 'w', 'h'], **d): (x0, y0), w, h = popfromkwargs(['xy', 'w', 'h'], d) points = np.array([[x0, y0], [x0 + w, y0 + h]]) elif allinkwargs(['center', 'w', 'h'], **d): (xc, yc), w, h = popfromkwargs(['center', 'w', 'h'], d) points = np.array([[xc - w/2, yc - h/2], [xc + w/2, yc + h/2]]) except Exception as e: print(e) return None return points
[docs] def region(self) -> Iterable: """ Returns the region as a list of 4 values x0, y0, w, and h, where x0 and y0 are the coordinates of the bottom-left corner, w and h are the width and height of the region. """ assert self.points is not None, "There are no points defined." return _points_to_region_(self.points)
[docs] def rhs(self, *, problem:NavierProblem=None) -> ndarray: """ Returns the coefficients as a NumPy array. Parameters ---------- problem : NavierProblem, Optional A problem the coefficients are generated for. If not specified, the attached problem of the object is used. Default is None. Returns ------- numpy.ndarray 2d float array of shape (H, 3), where H is the total number of harmonic terms involved (defined for the problem). """ p = problem if problem is not None else self.problem return rhs_rect_const(p.size, p.shape, self.value, self.points)
def __repr__(self): return 'RectangleLoad(%s)' % (dict.__repr__(self))
[docs]class LineLoad(LoadGroup): """ A class to handle loads over lines. Parameters ---------- x : Iterable The point of application as an 1d iterable for a beam, a 2d iterable for a plate. In the latter case, the first row is the first point, the second row is the second point. v : Iterable Load intensities for each dof. The order of the dofs for a beam is [F, M], for a plate it is [F, Mx, My]. """ _typestr_ = 'line' def __init__(self, *args, x:Iterable=None, v:Iterable=None, **kwargs): super().__init__(*args, x=x, v=v, **kwargs) @classmethod def _decode_(cls, d: dict = None, *args, **kwargs): if d is None: d = kwargs kwargs = None if kwargs is not None: d.update(kwargs) clskwargs = { 'key': d.pop('key', None), 'x': np.array(d.pop('x')), 'v': np.array(d.pop('v')), } clskwargs.update(d) return cls(**clskwargs) def _encode_(self, *args, **kwargs) -> dict: res = {} cls = type(self) res = { 'type': cls._typestr_, 'key': self.key, 'x': float_to_str_sig(self['x'], sig=6), 'v': float_to_str_sig(self['v'], sig=6), } return res
[docs] def rhs(self, *, problem:NavierProblem=None) -> ndarray: """ Returns the coefficients as a NumPy array. Parameters ---------- problem : NavierProblem, Optional A problem the coefficients are generated for. If not specified, the attached problem of the object is used. Default is None. Returns ------- numpy.ndarray 2d float array of shape (H, 3), where H is the total number of harmonic terms involved (defined for the problem). """ p = problem if problem is not None else self.problem x = np.array(self['x'], dtype=float) v = np.array(self['v'], dtype=float) return rhs_line_const(p.length, p.N, x, v)
def __repr__(self): return 'LineLoad(%s)' % (dict.__repr__(self))
[docs]class PointLoad(LoadGroup): """ A class to handle concentrated loads. Parameters ---------- x : Union[float, Iterable] The point of application. A scalar for a beam, an iterable of length 2 for a plate. v : Iterable Load values for each dof. The order of the dofs for a beam is [F, M], for a plate it is [F, Mx, My]. """ _typestr_ = 'point' def __init__(self, *args, x:Union[float, Iterable]=None, v:Iterable=None, **kwargs): super().__init__(*args, x=x, v=v, **kwargs) @classmethod def _decode_(cls, d: dict = None, *args, **kwargs): if d is None: d = kwargs kwargs = None if kwargs is not None: d.update(kwargs) clskwargs = { 'key': d.pop('key', None), 'x': np.array(d.pop('x')), 'v': np.array(d.pop('v')), } clskwargs.update(d) return cls(**clskwargs) def _encode_(self, *args, **kwargs) -> dict: res = {} cls = type(self) res = { 'type': cls._typestr_, 'key': self.key, 'x': float_to_str_sig(self['x'], sig=6), 'v': float_to_str_sig(self['v'], sig=6), } return res
[docs] def rhs(self, *, problem:NavierProblem=None) -> ndarray: """ Returns the coefficients as a NumPy array. Parameters ---------- problem : NavierProblem, Optional A problem the coefficients are generated for. If not specified, the attached problem of the object is used. Default is None. Returns ------- numpy.ndarray 2d float array of shape (H, 3), where H is the total number of harmonic terms involved (defined for the problem). """ p = problem if problem is not None else self.problem x = self['x'] v = np.array(self['v']) if hasattr(p, 'size'): return rhs_conc_2d(p.size, p.shape, v, x) else: return rhs_conc_1d(p.length, p.N, v, x)
def __repr__(self): return 'PointLoad(%s)' % (dict.__repr__(self))
def _points_to_region_(points: ndarray): xmin = points[:, 0].min() ymin = points[:, 1].min() xmax = points[:, 0].max() ymax = points[:, 1].max() return xmin, ymin, xmax - xmin, ymax - ymin