Source code for limitstates.objects.section.clt

"""
Classes that are used to represent CLT sections.


"""

from .section import SectionAbstract
from ..material import MaterialElastic
from ... units import ConverterStress, ConverterDensity, ConverterLength

from dataclasses import dataclass
import numpy as np

from typing import Protocol



class AbstractMaterialTimber(Protocol):
    """
    A protocol class that defines all the propreties needed for a CLT section.
    """
    fb: float
    fb90: float
    fv: float
    fv90: float
    E: float
    E90: float
    G: float
    G90: float

    def getE(self, isStrong:bool):
        """
        A method used to get E in the strong or weak direction.
        """
        pass

    def getG(self, isStrong:bool):
        """
        A method used to get G in the strong or weak direction.
        """
        pass
    
    def sConvert(self):
        """
        A method used to get the conversion factor between stress units.
        """
        pass

[docs]@dataclass class LayerClt: """ Represents a single layer of CLT. The Section is made up of an aggregate of layers, defined at differnt heights. """ t:float mat:AbstractMaterialTimber ymidfloat = None parallelToStrong:bool = True lUnit:str = 'mm' def __post_init__(self): self._initUnits(self.lUnit) def _initUnits(self, lUnit): """Initiates the length unit used for the layer""" self.lUnit = lUnit self.lConverter = ConverterLength()
[docs] def lConvert(self, outputUnit:str): """ Get the conversion factor from the current unit to the output unit for length units """ return self.lConverter.getConversionFactor(self.lUnit, outputUnit)
def __repr__(self): return f"<limitstates CLT layer {self.t}{self.lUnit} {self.mat.lamGrade}.>"
[docs] def getLayerE(self, checkInStrong:bool =True): """ Returns the E value depending on the layers orientation, and if the layer is being checked in the strong axis or weak axis. If the layers orientation matches, i.e. the layer is orientated in the weak direction and it's being checked in the weak direction, then the parallel elastic modulus is returned. Otherwise, the perpendicular to grain elastic modulus is returned. Parameters ---------- checkInStrong : bool A flag that specifies if we are lookin at the layers strong or weak axis. """ if self._layerMatchesDirection(checkInStrong): return self.mat.E else: return self.mat.E90
[docs] def getLayerG(self, checkInStrong:bool =True): """ Returns the G value for a global panel orientation. Parameters ---------- checkInStrong : bool A flag that specifies if we are lookin at the layers strong or weak axis. """ if self._layerMatchesDirection(checkInStrong): return self.mat.G else: return self.mat.G90
def _layerMatchesDirection(self, checkInStrong): return checkInStrong == self.parallelToStrong
[docs]class LayerGroupClt: """ Represents a group of CLT layers, and acts on them to find net section propreties. The CLT layers are numberd from top layer to bottom layer. Parameters ---------- layers : list[LayerClt] A list of the input layers to m. Returns ------- None. """ layers:list[LayerClt] ybar:float dnet:float grade:str def __init__(self, layers:list[LayerClt]): self.layers:list[LayerClt] = layers self._setLayerBoundaries() self._setLayerMidpointsAbs() self.d = self.lBoundaries[-1] self.lUnit = self.layers[0].lUnit self.lConvert = self.layers[0].lConvert self.sConvert = self.layers[0].mat.sConvert self.grade = self.layers[0].mat.grade
[docs] def getLayerAttr(self, attr:str) -> np.ndarray: out = [] for layer in self.layers: out.append(getattr(layer, attr)) return np.array(out)
def __repr__(self): return self.layers.__repr__() def __len__(self): return len(self.layers) def __getitem__(self, ii): return self.layers[ii]
[docs] def updateUnits(self, lUnit:str): """ Updates all the layers to have the new unit. """ self.lUnit = lUnit scaleFactor = self.layers[0].lConvert(lUnit) for layer in self.layers: layer.lUnit = lUnit layer.t = layer.t* scaleFactor self._setLayerBoundaries() self._setLayerMidpointsAbs() self.d = self.lBoundaries[-1]
def _setLayerBoundaries(self): y0 = 0 boundaries = [0] for layer in self.layers: y0 = y0 + layer.t boundaries.append(y0) self.lBoundaries = boundaries def _setLayerMidpointsAbs(self): lMidpointsAbs = [] for ii in range(len(self.layers)): lMidpointsAbs.append(self.lBoundaries[ii+1] - self.layers[ii].t/2) self.lMidpointsAbs = lMidpointsAbs
[docs] def getYbar(self, checkInStrong:bool =True) -> float: EAy = 0 EA = 0 for ii in range(len(self.layers)): layer = self.layers[ii] E = layer.getLayerE(checkInStrong) EAtemp = E * layer.t EA += EAtemp EAy += EAtemp*self.lMidpointsAbs[ii] return EAy / EA
def _getLayerMidpointsRelative(self, parallelToStrong:bool = True): lMidpointsRel = [] ybar = self.getYbar(parallelToStrong) for ii in range(len(self.layers)): lMidpointsRel.append(ybar - self.lMidpointsAbs[ii]) return lMidpointsRel
[docs] def getYmax(self, parallelToStrong:bool = True) -> float: ybar = self.getYbar(parallelToStrong) r1 = abs(ybar - self.lBoundaries[0]) r2 = abs(ybar - self.lBoundaries[-1]) return max(r1, r2)
[docs] def getEI(self, parallelToStrong:bool = True, sUnit:str = 'Pa', lUnit:str = 'm'): """ Gets EI for the layer group in the given global orientation. returns per unit, not net. Parameters ---------- globalOrientation : float The orientation of the global direction to get EI in. lUnit : str, optional The length units for EI. The default is 'm'. sUnit : str, optional The stress units for EI. The default is 'Pa'. Returns ------- float EI for the section. """ EI = 0 lMid = self._getLayerMidpointsRelative(parallelToStrong) for ii, layer in enumerate(self.layers): E = layer.getLayerE(parallelToStrong) EI += layer.t**3 * E / 12 EI += layer.t * lMid[ii]**2 * E sfactor = self.sConvert(sUnit) lfactor = self.lConvert(lUnit) return EI * sfactor * lfactor**3
[docs] def getGA(self, parallelToStrong:bool = True, NlayerTotal:int = None, sUnit:str = 'Pa',lUnit:str = 'm'): """ Gets GA for the layer group orientation. Parameters ---------- parallelToStrong : bool A flag that is set to true if we are looking in the strong axis. NlayerTotal : int The total number of "active" layers in the section. If not set, defaults to the total number of layers, which is correct if looking at the strong axis. In the weak axis, this may be a different number of layers. sUnit : str, optional The stress units for EI. The default is 'Pa'. lUnit : str, optional The length units for EI. The default is 'm'. Returns ------- float GA for the section in the input units. """ layers = self.layers Nlayer = len(layers) if not NlayerTotal: NlayerTotal = Nlayer denom = 0 h = 0 # Get the first terms of the denominator. G0 = layers[0].getLayerG(parallelToStrong) t0 = layers[0].t/2 denom += t0/G0 h += t0 # account for the final layer if it's present. if NlayerTotal == Nlayer: GN = layers[NlayerTotal-1].getLayerG(parallelToStrong) tN = layers[NlayerTotal-1].t / 2 denom += tN/GN h += tN # middle terms. for ii in range(1, Nlayer-1): layer = layers[ii] G = layer.getLayerG(parallelToStrong) denom += layer.t / G h += layer.t GA = h**2 / denom sfactor = self.sConvert(sUnit) lfactor = self.lConvert(lUnit) return GA * sfactor * lfactor
[docs] def getEA(self, parallelToStrong:bool = True, lUnit:str = 'm', sUnit:str = 'Pa') -> bool: """ Gets EI for the layer group in the given global orientation. Parameters ---------- globalOrientation : float The orientation of the global direction to get EI in. lUnit : str, optional The length units for EI. The default is 'm'. sUnit : str, optional The stress units for EI. The default is 'Pa'. Returns ------- TYPE DESCRIPTION. """ EA = 0 # lMid = self._getLayerMidpointsRelative(parallelToStrong) for ii, layer in enumerate(self.layers): E = layer.getLayerE(parallelToStrong) EA += layer.t * E sfactor = self.sConvert(sUnit) lfactor = self.lConvert(lUnit) return EA * sfactor * lfactor
[docs] def getLayerOrientations(self, parallelToStrong:bool = True) -> list[bool]: """ Gets the layer orientations in the given global direction. to the strong or weak direction. Parameters ---------- globalOrientation : bool The orientation of the global direction to check the layers are parallel to. Returns ------- list[bool] True means the layer is parallel to global direction it's being checked in. """ return [layer._layerMatchesDirection(parallelToStrong) for layer in self.layers]
# ============================================================================= # # ============================================================================= class SectionLayered(SectionAbstract): """ Represents a layered section, for example CLT. """ def getEA(sUnit='Pa', lUnit='m'): pass def getEIx(sUnit='Pa', lUnit='m'): pass def getEIy(sUnit='Pa', lUnit='m'): pass def getGAx(sUnit='Pa', lUnit='m'): pass def getGAy(sUnit='Pa', lUnit='m'): pass def _isPerpendicular(searchInStrong, layer): return searchInStrong != layer.parallelToStrong def getActiveLayers(LayerGroupClt:LayerGroupClt, searchInStrong=True) -> LayerGroupClt: """ A function that can be used to determine the "active" layers of CLT. Layers perpendicular to the direction of interest are neglected if they are on the outside of the CLT layer group. Parameters ---------- LayerGroupClt : LayerGroupClt The input layer group to check. searchInStrong : bool, optional A flag that can be used to get active layers in either the strong or weak direction are polling in the strong or weak axis directions. Returns ------- LayerGroupClt DESCRIPTION. """ #remove all empty layers, this is applicable in the case of a fire section. layers = [layer for layer in LayerGroupClt.layers if layer.t !=0] Nlayers = len(layers) # Ignore all top layers that aren't parallel ii = 0 while _isPerpendicular(searchInStrong, layers[ii]): ii += 1 # Stop iterating when no layers are left. if ii == Nlayers: break # Ignore all bottom layers that aren't parallel jj = -1 while _isPerpendicular(searchInStrong, layers[jj]): if jj == -Nlayers: break jj -= 1 if jj == -1: return layers[ii:] else: return layers[ii:(jj+1)]
[docs]class SectionCLT(SectionLayered): """ Represents a layered CLT object. Layers can have strong axis direction and weak axis direction. CLT sections have two effective layer groups in each direction. If the user is representing a section that is reduced from the original section dimensions, as is the case, then NlayersTotal needs to be set to the original number of layers the section has. Units of the CLT sectionare the same as the untis for the the layers used. If the length unit, lUnit is set be differnt than default, then the units of the layers will be updates as well. Parameters ---------- layers : list[LayerClt] The group of CLT layers to use for the section. w : float, optional The width of the section to use for design propreties. The default is a unit width, or 1000. wWeak : float, optional The width of the section to use for it's weak axis design propreties. The default is to use the same width as the strong axis. lUnit : string, optional The width units to use for the section. The using 'default' sets units to the same as the layer group. If another unit is specified, then the units for each layer is converted to this new unit. NlayerTotal : int, optional The total number of layers in the original section, pre-fire. GA is dependant on not just the layers that are active, but the total number of layers in the CLT. Returns ------- None. """ sLayers:LayerGroupClt wLayers:LayerGroupClt def __init__(self, layers:LayerGroupClt, w:float = 1000, wWeak:float = None, lUnit = 'default', NlayerTotal = None): # set the weak axis width equal to w if it's not set. if not wWeak: wWeak = w # use the same units as the layers if not set. if lUnit == 'default': self.lUnit = layers[0].lUnit self._initUnits(layers[0].lUnit) else: self.lUnit = lUnit self._initUnits(lUnit) # If the layers don't match, update them. if layers[0].lUnit != lUnit: layers.updateUnits(lUnit) # what does this do? # self.w = w / self.lConvert('mm') # self.wWeak = wWeak / self.lConvert('mm') self.w = w self.wWeak = wWeak if not NlayerTotal: NlayerTotal = len(layers) self.NlayerTotal = NlayerTotal self.layers = layers # the base section shouldn't be used for design. self.sLayers = LayerGroupClt(getActiveLayers(layers,True)) self.wLayers = LayerGroupClt(getActiveLayers(layers,False)) def _initUnits(self, lUnit): """Initiates the length unit used for the layer""" self.lUnit = lUnit self.lConverter = ConverterLength() @property def name(self): return f'{self.sLayers.grade} {int(self.sLayers.d)}' def __repr__(self): return f'<limitstates CLT {self.name} Section>'
[docs] def lConvert(self, outputUnit:str): """ Get the conversion factor from the current unit to the output unit for length units Parameters ---------- outputUnit : str The unit to get the conversion factor to. Returns ------- float The conversion factor between the current length unit and the target output length unit. """ return self.lConverter.getConversionFactor(self.lUnit, outputUnit)
def _convertUnits(self, lUnit): """Initiates the length unit used for the layer""" self.lUnit = lUnit factor = self.lConvert('mm') self.w = self.w / factor self.wWeak = self.wWeak / factor self.sLayers.updateUnits(lUnit) self.wLayers.updateUnits(lUnit)
[docs] def getEAs(self, sUnit='Pa', lUnit='m'): raise NotImplementedError('Returning EA is still in development.')
[docs] def getEAw(self, sUnit='Pa', lUnit='m'): raise NotImplementedError('Returning EA is still in development.')
[docs] def getEIs(self, sUnit='Pa', lUnit='m'): """ Returns EI about the sections strong axis. Returns in units of sUnit x lUnit^4 Parameters ---------- lUnit : float, optional The length units to output Ix in. The default is 'm'. sUnit : float, optional Stress units to output E in. The default is 'Pa'. Returns ------- float. The EIs for the section. """ lconvertWidth = self.w*self.lConvert(lUnit) return self.sLayers.getEI(True, sUnit, lUnit)*lconvertWidth
[docs] def getEIw(self, sUnit='Pa', lUnit='m'): """ Returns EI about the sections weak axis. Returns in units of sUnit x lUnit^4 Parameters ---------- lUnit : float, optional The length units to output Ix in. The default is 'm'. sUnit : float, optional Stress units to output E in. The default is 'Pa'. Returns ------- float. The EIw for the section. """ lconvertWidth = self.w*self.lConvert(lUnit) return self.wLayers.getEI(False, sUnit, lUnit)*lconvertWidth
[docs] def getGAs(self, sUnit='Pa', lUnit='m'): """ Returns GA about the sections strong axis. Returns in units of sUnit x lUnit^2 Parameters ---------- lUnit : float, optional The length units to output As in. The default is 'm'. sUnit : float, optional Stress units to output E in. The default is 'Pa'. Returns ------- float. The GAs for the section. """ lconvertWidth = self.w*self.lConvert(lUnit) Nlayer = self.NlayerTotal return self.sLayers.getGA(True, Nlayer, sUnit, lUnit)*lconvertWidth
[docs] def getGAw(self, sUnit='Pa', lUnit='m'): """ Returns GA about the sections weak axis. Returns in units of sUnit x lUnit^2 Parameters ---------- lUnit : float, optional The length units to output As in. The default is 'm'. sUnit : float, optional Stress units to output E in. The default is 'Pa'. Returns ------- float. The GAw for the section. """ lconvertWidth = self.w*self.lConvert(lUnit) Nlayer = self.NlayerTotal return self.sLayers.getGA(False, Nlayer, sUnit, lUnit)*lconvertWidth