Writing Custom Models

Writing custom theoretical models is a powerful, extensible option of the LamAna package.

Authoring Custom Models

Custom models are simple .py files that can be locally placed by the user into the models directory. The API finds these selected files from the apply(model='<name>') method in the distributions.Case class. In order for these processes to work smoothly, the following essentials are needed to “handshake” with theories module.

  1. Implement a _use_model_() hook that returns (at minimum) an updated DataFrame.
  2. If using the class-style models, implement _use_model_() hook within a class that inherits from theories.BaseModel.

Exceptions for specific models are maintained by the models author.

Which style do I implement?

  • For beginners, function-style models are the best way to start making custom models.
  • We recommend class-style models in general, which use object-oriented principles such as inheritance. This is best suited for intermediate Pythonistas, which we encourage everyone to consider acheiving. :)

The following cell shows an excerpt of the class-style model.

Examples of function-style and class-style models are found in the “examples” folder of the repository.

#------------------------------------------------------------------------------
# Class-style model

# ...

class Model(BaseModel):
    '''A custom CLT model.

    A modified laminate theory for circular biaxial flexure disks,
    loaded with a flat piston punch on 3-ball support having two distinct
    materials (polymer and ceramic).

    '''
    def __init__(self):
        self.Laminate = None
        self.FeatureInput = None
        self.LaminateModel = None

    def _use_model_(self, Laminate, adjusted_z=False):
        '''Return updated DataFrame and FeatureInput.

        ...

        Returns
        -------
        tuple
            The updated calculations and parameters stored in a tuple
            `(LaminateModel, FeatureInput)``.

            df : DataFrame
                LaminateModel with IDs and Dimensional Variables.
            FeatureInut : dict
                Geometry, laminate parameters and more.  Updates Globals dict for
                parameters in the dashboard output.


        '''
        self.Laminate = Laminate
        df = Laminate.LFrame.copy()
        FeatureInput = Laminate.FeatureInput

        # Author-defined Exception Handling
        if (FeatureInput['Parameters']['r'] == 0):
            raise ZeroDivisionError('r=0 is invalid for the log term in the moment eqn.')
        elif (FeatureInput['Parameters']['a'] == 0):
            raise ZeroDivisionError('a=0 is invalid for the log term in the moment eqn.')

        # ...

        # Calling functions to calculate Qs and Ds
        df.loc[:, 'Q_11'] = self.calc_stiffness(df, FeatureInput['Properties']).q_11
        df.loc[:, 'Q_12'] = self.calc_stiffness(df, FeatureInput['Properties']).q_12
        df.loc[:, 'D_11'] = self.calc_bending(df, adj_z=adjusted_z).d_11
        df.loc[:, 'D_12'] = self.calc_bending(df, adj_z=adjusted_z).d_12

        # Global Variable Update
        if (FeatureInput['Parameters']['p'] == 1) & (Laminate.nplies%2 == 0):
            D_11T = sum(df['D_11'])
            D_12T = sum(df['D_12'])
        else:
            D_11T = sum(df.loc[df['label'] == 'interface', 'D_11']) # total D11
            D_12T = sum(df.loc[df['label'] == 'interface', 'D_12'])
        #print(FeatureInput['Geometric']['p'])

        D_11p = (1./((D_11T**2 - D_12T**2)) * D_11T)         #
        D_12n = -(1./((D_11T**2 - D_12T**2))  *D_12T)        #
        v_eq = D_12T/D_11T                                   # equiv. Poisson's ratio
        M_r = self.calc_moment(df, FeatureInput['Parameters'], v_eq).m_r
        M_t = self.calc_moment(df, FeatureInput['Parameters'], v_eq).m_t
        K_r = (D_11p*M_r) + (D_12n*M_t)                    # curvatures
        K_t = (D_12n*M_r) + (D_11p*M_t)

        # Update FeatureInput
        global_params = {
            'D_11T': D_11T,
            'D_12T': D_12T,
            'D_11p': D_11p,
            'D_12n': D_12n,
            'v_eq ': v_eq,
            'M_r': M_r,
            'M_t': M_t,
            'K_r': K_r,
            'K_t:': K_t,
        }

        FeatureInput['Globals'] = global_params
        self.FeatureInput = FeatureInput                   # update with Globals
        #print(FeatureInput)

        # Calculate Strains and Stresses and Update DataFrame
        df.loc[:,'strain_r'] = K_r * df.loc[:, 'Z(m)']
        df.loc[:,'strain_t'] = K_t * df.loc[:, 'Z(m)']
        df.loc[:, 'stress_r (Pa/N)'] = (df.loc[:, 'strain_r'] * df.loc[:, 'Q_11']
                                ) + (df.loc[:, 'strain_t'] * df.loc[:, 'Q_12'])
        df.loc[:,'stress_t (Pa/N)'] = (df.loc[:, 'strain_t'] * df.loc[:, 'Q_11']
                             ) + (df.loc[:, 'strain_r'] * df.loc[:, 'Q_12'])
        df.loc[:,'stress_f (MPa/N)'] = df.loc[:, 'stress_t (Pa/N)']/1e6

        del df['Modulus']
        del df['Poissons']

        self.LaminateModel = df

        return (df, FeatureInput)


    #  Add Defaults here

Note

DEV: If testing with both function- and class-styles, keep in mind any changes to the model should be reflected in both styles.

What are Defaults?

Recall there are a set of geometric, loading and material parameters that are required to run LT calculations. For testing purposes, these parameters can become tedious to set up each time you wish to run a simple plot or test parallel case. Therefore, you can prepare variables that store default parameters with specific values. Calling these variables can reduce the redundancy of typing them over again.

Simply inherit from BaseDefaults. The BaseDefaults class stores a number of common geometry strings, Geometry objects, arbitrary loading parameters and material properties. These values are intended to get you started, but can be altered easily to fit your model. In addition, this class has methods for easily building accepted, formatted FeatureInput objects.

class Defaults(BaseDefaults):
    '''Return parameters for building distributions cases.  Useful for consistent
    testing.

    Dimensional defaults are inherited from utils.BaseDefaults().
    Material-specific parameters are defined here by he user.

    - Default geometric parameters
    - Default material properties
    - Default FeatureInput

    Examples
    ========
    >>> dft = Defaults()
    >>> dft.load_params
    {'R' : 12e-3, 'a' : 7.5e-3, 'p' : 1, 'P_a' : 1, 'r' : 2e-4,}

    >>> dft.mat_props
    {'Modulus': {'HA': 5.2e10, 'PSu': 2.7e9},
    'Poissons': {'HA': 0.25, 'PSu': 0.33}}

    >>> dft.FeatureInput
     {'Geometry' : '400-[200]-800',
      'Geometric' : {'R' : 12e-3, 'a' : 7.5e-3, 'p' : 1, 'P_a' : 1, 'r' : 2e-4,},
      'Materials' : {'HA' : [5.2e10, 0.25], 'PSu' : [2.7e9, 0.33],},
      'Custom' : None,
      'Model' : Wilson_LT}

    '''
    def __init__(self):
        BaseDefaults.__init__(self)
        '''DEV: Add defaults first.  Then adjust attributes.'''
        # DEFAULTS ------------------------------------------------------------
        # Build dicts of geometric and material parameters
        self.load_params = {
            'R': 12e-3,                                    # specimen radius
            'a': 7.5e-3,                                   # support ring radius
            'p': 5,                                        # points/layer
            'P_a': 1,                                      # applied load
            'r': 2e-4,                                     # radial distance from center loading
        }

        self.mat_props = {
            'Modulus': {'HA': 5.2e10, 'PSu': 2.7e9},
            'Poissons': {'HA': 0.25, 'PSu': 0.33}
        }

        # ATTRIBUTES ----------------------------------------------------------
        # FeatureInput
        self.FeatureInput = self.get_FeatureInput(
            self.Geo_objects['standard'][0],
            load_params=self.load_params,
            mat_props=self.mat_props,
            model='Wilson_LT',
            global_vars=None
        )

Handling Model Exceptions (0.4.3c6)

Since users can create their own models and use them in LamAna, it becomes important to handle erroroneous code. The oneous of exception handling is maintained by the model’s author. However, basic handling is incorporated within Laminate._update_calculations to prevent erroroneous code from halting LamAna. In other words, provided the variables for Laminate construction are valid, a Laminate will be stored and accessed via Laminate.LFrame. This again is the a primitive DataFrame with IDs and Dimensional data prior to updating. When _update_cacluations() is called and any exception is raised, they are caught and LFrame is set to LMFrame, allowing other dependency code to work. A traceback will still print even though the exception was caught, allowing the author to improve their code and prevent breakage. LMFrame will not update unless the author model code lacks exceptions.

Again, primary exception handling of models is the author’s responsibility.

Modified Classical Laminate Theory - Wilson_LT

Here is a model that comes with LamAna. It applies Classical Laminate Theory (CLT) to circular-disk laminates with alternating ceramic-polymer materials. CLT was modified for disks loaded in biaxial flexure.

Stiffness Matrix: \(E\) is elastic modulus, \(\nu\) is Poisson’s ratio.

\[\begin{split}|Q| = \begin{vmatrix} Q_{11}& Q_{12}\\ Q_{21}& Q_{22} \end{vmatrix}\end{split}\]
\[Q_{11}=Q_{22}=E/(1-\nu^2)\]
\[Q_{12}=Q_{21}=\nu E/(1-\nu^2)\]

Bending : \(k\) is essentially the enumerated interface where \(k=0\) is tensile surface. \(h\) is the layer thickness relative to the neutral axis where \(t_{middle} = h_{middle}/2\). \(z\) (lower case) is the relative distance betweeen the neuatral axis and a lamina centroid.

\[\begin{split}|D| = \begin{vmatrix} D_{11}& D_{12}\\ D_{21}& D_{22} \end{vmatrix}\end{split}\]
\[D_{11}=D_{22}=\Sigma_{k=1}^N Q_{11(k)}((h_{(k)}^3/12)+h_{(k)}z_{(k)}^2)\]
\[D_{12}=D_{21}=\Sigma_{k=1}^N Q_{12(k)}((h_{(k)}^3/12)+h_{(k)}z_{(k)}^2)\]

Equivalent Poisson’s Ratio

\[\nu_{eq} = D_{12}/D_{11}\]

Moments: radial and tangential bending moments. The tangential stress is used for the failure stress.

\[M_r = (P/4\pi)[(1+\nu_{eq})\log(a/r)]\]
\[M_t = (P/4\pi)[(1+\nu_{eq})\log(a/r)+(1-\nu_{eq})]\]

Curvature

\[\begin{split} \begin{Bmatrix} K_r \\ K_t \end{Bmatrix} = [D]^{-1} \begin{Bmatrix} M_r \\ M_t \end{Bmatrix}\end{split}\]

Strain: \(Z\) (caplital) is the distance betwen the neutral axis and the lamina interface.

\[\begin{split} \begin{Bmatrix} \epsilon_r \\ \epsilon_t \end{Bmatrix} = Z_k \begin{Bmatrix} K_r \\ K_t \end{Bmatrix}\end{split}\]

Stress

\[\begin{split} \begin{Bmatrix} \sigma_r \\ \sigma_t \end{Bmatrix} = [Q] \begin{Bmatrix} \epsilon_r \\ \epsilon_t \end{Bmatrix}\end{split}\]