content: #------------------------------------------------------------------------------
  '''Class-style Model
  
  This fixture is used to test the importing of models, handled by the
  `theories.handshake()` module.  As of 0.4.11, models can:
  
  - be located in the `lamana.models` folder
  - module and classes can have any pythonic name; hard-coding removed
  - any sub-package can be accessed by the "model" keyword in `Case.apply()`
  - search for the hook-containing class and it's hook method
  
  This module is here to test these aspects as the module is imported.  The Wilson_LT
  model was adapted.  No functions are expected in this module; there are tests
  against this.
  
  '''
  
  import math
  import collections as ct
  
  import pandas as pd
  
  from lamana.input_ import BaseDefaults
  from lamana.theories import BaseModel
  from lamana.lt_exceptions import IndeterminateError
  
  
  # This class lacks a hook method; theories should skip it.
  class DummyModel():
  	pass
  
  
  # The class containing the hook method can have any name.
  class RandomName(BaseModel):
      '''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).'''
  
      '''Accept extra args and kwds here'''
      def __init__(self):
          self.Laminate = None
          self.FeatureInput = None
          self.LaminateModel = None
  
      # TODO: eventually abstract into BaseModel and deprecate direct coding
      # TODO: accept kwargs from Case -> handshake
      def _use_model_(self, Laminate, adjusted_z=False):
          '''Return updated DataFrame and FeatureInput Return None if exceptions raised.
  
          Parameters
          ----------
          df : DataFrame
              LaminateModel with IDs and Dimensional Variables.
          FeatureInut : dict
              Geometry, laminate parameters and more.  Updates Globals dict for
              parameters in the dashboard output.
          adjusted_z: bool; default=False
              If True, uses z(m)* values instead; different assumption for internal calc.
  
          Raises
          ------
          ZeroDivisionError
              If zero `r` or `a` in the log term are zero.
          ValueError
              If negative numbers are in the log term or the support radius exceeds
              the sample radius.
  
          Returns
          -------
          tuple
              The updated calculations and parameters stored in a tuple
              `(LaminateModel, FeatureInput)``.
  
          '''
          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.')
          elif (FeatureInput['Parameters']['r'] < 0) | (FeatureInput['Parameters']['a'] < 0):
              raise ValueError('Negative numbers are invalid for the log term '
                               'in moment eqn.')
          elif FeatureInput['Parameters']['a'] > FeatureInput['Parameters']['R']:
              raise ValueError('Support radius is larger than sample radius.')
          elif df['side'].str.contains('INDET').any():
              print('INDET value found.  Rolling back...')
              raise IndeterminateError('INDET value found. Unable to accurately calculate stress.')
              #raise AssertionError('Indeterminate value found.  Unable to accurately calculate stress.')
  
          # 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)
  
      #------------------------------------------------------------------------------
      '''Prefer staticmethods here.  Add formulas to doc strings.'''
      def calc_stiffness(self, df, mat_props):
          '''Return tuple of Series of (Q11, Q12) floats per lamina.'''
          # Iterate to Apply Modulus and Poisson's to correct Material
          # TODO: Prefer cleaner ways to parse materials from mat_props
          df_mat_props = pd.DataFrame(mat_props)             # df easier to munge
          df_mat_props.index.name = 'materials'
          ##for material in mat_props.index:
          for material in df_mat_props.index:
              mat_idx = df['matl'] == material
              df.loc[mat_idx, 'Modulus'] = df_mat_props.loc[material, 'Modulus']
              df.loc[mat_idx, 'Poissons'] = df_mat_props.loc[material, 'Poissons']
              E = df['Modulus']                              # series of moduli
              v = df['Poissons']
          stiffness = ct.namedtuple('stiffness', ['q_11', 'q_12'])
          q_11 = E / (1 - (v**2))
          q_12 = (v*E) / (1 - (v**2))
          return stiffness(q_11, q_12)
  
      def calc_bending(self, df, adj_z=False):
          '''Return tuple of Series of (D11, D12) floats.'''
          q_11 = df['Q_11']
          q_12 = df['Q_12']
          h = df['h(m)']
          # TODO: need to fix kwargs passing first; tabled since affects many modules.
          if not adj_z:
              z = df['z(m)']
          else:
              z = df['z(m)*']
          bending = ct.namedtuple('bending', ['d_11', 'd_12'])
          d_11 = ((q_11*(h**3)) / 12.) + (q_11*h*(z**2))
          d_12 = ((q_12*(h**3)) / 12.) + (q_12*h*(z**2))
          return bending(d_11, d_12)
  
      def calc_moment(self, df, load_params, v_eq):
          '''Return tuple of moments (radial and tangential); floats.
          See Timishenko-Woinowsky: Eq. 91; default'''
          P_a = load_params['P_a']
          a = load_params['a']
          r = load_params['r']
          moments = ct.namedtuple('moments', ['m_r', 'm_t'])
          m_r = ((P_a/(4*math.pi)) * ((1 + v_eq)*math.log10(a/r)))
          m_t = ((P_a/(4*math.pi)) * (((1 + v_eq)*math.log10(a/r)) + (1 - v_eq)))
          return moments(m_r, m_t)
  
  
  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}
  
      Returns
      -------
      class
          Updated attributes inherited from  the `BaseDefaults` class.
  
      '''
      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,
              ##custom_matls=None,
              model='Wilson_LT',
              global_vars=None
          )
  