<commit_before># -----------------------------------------------------------------------------
'''Classes and functions for handling user inputs.'''
# Geometry(): parse user input geometry strings to a tuple of floats (and lists/str)
# BaseDefaults(): library of general geometry defaults; subclassed by the user.
# flake8 input_.py --ignore=E265, E501, N803, N806, N802, N813, E133

import itertools as it
import collections as ct

import pandas as pd

import lamana as la
from lamana.utils import tools as ut

# =============================================================================
# USER INPUT ------------------------------------------------------------------
# =============================================================================
# Parses informations and generates User Input objects, i.e. Geometry()


class Geometry(object):
    '''Parse input geometry string into floats.

    When a single (or a list of) geometry string(s) is passed to the
    `lamana.distributions.Case.apply()` method, this class parses those strings
    into (outer, [inner], middle, 'S') format'; 'S' is optional.

    Here are examples of conventions used to write geometry strings:

    - General: outer-[inner_i]-middle
    - Short-hand: outer-inner-middle

    Formatted in General Convention, a converted namedtuple of the geometry
    is returned.  Examples of GeometryTuples:

    - (400.0, [200.0], 800.0)                              # multi-ply
    - (400.0, [200.0], 800.0, 'S')                         # multi-ply, symmetric
    - (400.0, [100.0, 100.0], 800.0)                       # multi-ply, [inner_i]

    Parameters
    ----------
    geo_input : list or tupled list
        Geometry thicknesses of a laminate.

    Attributes
    ----------
    total
    total_middle
    total_inner
    total_inner_i
    total_outer
    is_symmetric
    namedtuple : namedtuple
        A GeometryTuple in General Convention, e.g. (400.0, [200.0], 800.0, 'S').
    geometry : list
        The converted and parsed geometry string.
    middle : float
        Middle layer thickness.
    inner : list of floats
        Inner layer thicknesses in micrometers.
    outer : float
        Outer layer thickness.
    string : str
        The input geometry string converted to General Convention format.

    Examples
    --------
    >>> g1 = ('0-0-2000')                                  # Monolith
    >>> g2 = ('1000-0-0')                                  # Bilayer
    >>> g3 = ('600-0-800')                                 # Trilayer
    >>> g4 = ('500-500-0')                                 # 4-ply
    >>> g5 = ('400-200-800')                               # Short-hand; <= 5-ply
    >>> g6 = ('400-200-400S')                              # Symmetric
    >>> g7 = ('400-[200]-800')                             # Gen. convention; 5-ply
    >>> g8 = ('400-[100,100]-800')                         # Gen. convention; 7-plys
    >>> la.input_.Geometry(g5)
    Geometry object (400.0-[200.0]-800.0)

    '''

    def __init__(self, geo_input):
        '''Ideally a want to call Geometry and get namedtuple auto; return self?'''

        # TODO: Consolidate into namedtuple or self
        # TODO: rename geometrytuple
        self.namedtuple = self._parse_geometry(geo_input)  # a namedtuple; see collections lib
        self.geometry = self._parse_geometry(geo_input)    # a namedtuple; see collections lib
        self.middle = self.geometry.middle                 # attributes from namedtuple; symmetric sensitive
        self.inner = self.geometry.inner
        self.outer = self.geometry.outer

        self.string = self.__class__._to_gen_convention(geo_input)

        # Private attribute used for set comparisons and hashing
        self._geometry_hash = self._parse_geometry(geo_input, hash_=True)

    def __str__(self):
        '''Returns trimmed geometry string.'''
        return self.string

    def __repr__(self):                                    # for object calls
        '''Returns Geometry object string.'''
        return '{} object ({})'.format(self.__class__.__name__, self.string)

    def __eq__(self, other):
        '''Compare GeometryTuples.'''
        if isinstance(other, self.__class__):
            #print(self.__dict__)
            #print(other.__dict__)
            return self.__dict__ == other.__dict__
        return NotImplemented

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is NotImplemented:
            return result
        return not result

    def __hash__(self):
        '''Allow set comparisons.

        The only required property for a hash is that objects which equally
        compare have the same hash value (REF 035).  `self.__dict__` is unhashable
        due to the inner list (lists are mutable, thus unhashable).  So a copy is
        made called `_geometry_hash` of GeometryTuple where the inner value is
        tupled instead.

        '''
        return hash(self._geometry_hash)
        #return hash(tuple(sorted(self.__dict__.items())))
        #return hash(self._geo_string)

    def _parse_geometry(self, geo_input, hash_=False):
        '''Return a namedtuple of outer-inner-middle geometry values.

        Per General Convention, a GeometryTuple has floats and an inner list.
        Checks for symmetry, handles inner list, then makes the GeometryTuple.

        Also can create a hashable version of GeometryTuple; tuple instead of
        list for inner_i.

        Parameters
        ---------
        geo_input : tupled str or str
            outer-inner-middle values or outer-[inner]-middle values.

        Returns
        -------
        namedtuple of mixed types
            GeometryTuple: numeric values converted to floats; (outer, [inner], middle,'S?')
        '''

        def check_symmetry(last):
            '''Yield float or str if 'S' is in the last token of the geometry string.

            Examples
            --------
            >>> [i for i in check_symmetry('400S')]
            [400.0, 'S']
            >>> [i for i in check_symmetry('400')]
            [400.0]
            '''
            if last.endswith('S'):
                # Converts last '400S' to [400.0,'S']
                #print(type(last))
                number = float(last[:-1])
                letter = last[-1]
                yield number
                yield letter
            else:
                # Converts last '400' to [400.0]
                yield float(last)

        # Inner Parsing
        def parse_inner(inside):
            '''Yield values from the inner list of a geometry string.

            Also parses inner_i (if multiple inners are found) str to floats.
            This is later converted to a list of inner_i, as required by LPEP 001.01.

            Examples
            --------
            >>> list(parse_inner('[100,100]'))
            [100.0, 100.0]
            >>> list(parse_inner('[200]'))
            [200.0]
            >>> list(parse_inner('200'))
            [200.0]

            Raises
            ------
            TypeError
                If a non-string is passed in for the geo_input arg.
            Exception
                If the parsed geo string is less than 3 tokens.

            '''
            if inside.startswith('['):
                if ',' in inside:
                    # Convert inside string '[100,100]' to [100.0,100.0]
                    for item in inside[1:-1].split(','):
                        yield float(item)
                else:
                    # Convert inside string '[200]' to [200.0]
                    converted = inside[1:-1]               # excludes bracket strings
                    #print(converted)
                    yield float(converted)
            else:
                # Convert insde string '200' to [200] if brackets aren't found
                yield float(inside)

        # Convert Input String
        def _make_GeometryTuple(tokens, hashable=False):
            '''Return a namedtuple of floats representing geometry thicknesses.'''
            outer = float(tokens[0])
            inner_hash = tuple(parse_inner(tokens[1]))     # tupled inner for hash()
            inner = list(parse_inner(tokens[1]))           # complies w LPEP 001.01
            middle = tuple(check_symmetry(tokens[-1]))

            '''Should warn the object is symmetric though.'''
            if 'S' not in geo_input:
                GeometryTuple = ct.namedtuple(
                    'GeometryTuple', ['outer', 'inner', 'middle'])
                if hashable:
                    return GeometryTuple(outer, inner_hash, middle[0])
                return GeometryTuple(outer, inner, middle[0])
            else:
                GeometryTuple = ct.namedtuple(
                    'GeometryTuple', ['outer', 'inner', 'middle', 'symmetric'])
                if hashable:
                    return GeometryTuple(outer, inner_hash, middle[0], middle[-1])
                return GeometryTuple(outer, inner, middle[0], middle[-1])

        # Create --------------------------------------------------------------
        '''Replace with try-except block.'''
        # Crude exception handling
        tokens = geo_input.split('-')
        if not isinstance(geo_input, str):
            raise TypeError("Cannot parse input type.  Supported types: str")
        elif len(tokens) < 3:
                # TODO: Replace with custom exception
                raise Exception("Input token is too short. Supported geometry string format: 'outer-[inner_i]-middle'")
        else:
            '''Find another name for hash_ which is a bool'''
            # Gets hash_ bool from parsed_geometry
            return _make_GeometryTuple(tokens, hashable=hash_)

    @classmethod
    def _to_gen_convention(cls, geo_input):
        '''Return a geometry string converted to general convention.'''
        tokens = geo_input.split('-')
        first = tokens[0]
        inside = tokens[1]
        last = tokens[-1]
        #print(inside)

        # Convert strings to general convention
        first = str(float(first))

        if inside.startswith('['):
            # Convert inside string '[100,100]' to '100.0,100.0'
            if ',' in inside:
                converted = [str(float(item)) for item in inside[1:-1].split(',')]
                inside = ','.join(converted)
            else:
                # Convert inside string '[200]' to '200.0'
                inside = str(float(inside[1:-1]))
        elif not inside.startswith('['):
            # Convert inside string '200' to 200.0 if brackets aren't found
            inside = str(float(inside))
        # Always add brackets
        inside = ''.join(['[', inside, ']'])
        #print('Converting geometry string to general convention.')

        if last.endswith('S'):
            last = str(float(last[:-1]))
            last = ''.join([last, 'S'])
        elif not last.endswith('S'):
            last = str(float(last))

        geo_string = '-'.join([first, inside, last])
        return geo_string

    # API --------------------------------------------------------------------
    # DEPRECATED: itotal() in 0.4.3d
    @property
    def total(self):
        '''Calculate total thickness for laminates using any convention.'''
        if self.is_symmetric:
            factor = 2
        else:
            factor = 1
        return 2 * self.outer + 2 * sum(self.inner) + factor * self.middle

    @property
    def total_middle(self):
        '''Calculate total thickness for middle lamina using any convention.'''
        if self.is_symmetric:
            return 2 * self.middle
        else:
            return self.middle

    @property
    def total_inner(self):
        '''Calculate total thickness for inner lamina using any convention.'''
        return 2 * sum(self.inner)

    @property
    def total_inner_i(self):
        '''Calculate total thickness for the ith inner lamina.'''
        result = [inner_i * 2 for inner_i in self.inner]
        return result

    @property
    def total_outer(self):
        '''Calculate total thickness for outer lamina using any convention.'''
        return 2 * self.outer

    @property
    def is_symmetric(self):
        '''Return True if 'S' convention is used.

        Examples
        --------
        >>> g5 = ('400-200-800')                           # Short-hand use; <= 5-ply
        >>> g6 = ('400-200-400S')                          # Symmetric
        >>> for geo in [g5, g6]
        ...     geo.is_symmetric
        False True
        '''
        return 'S' in self.geometry


# -----------------------------------------------------------------------------
# UTILITY
# -----------------------------------------------------------------------------
# Supplies basic dicts and methods by supplying custom case-building information.

class BaseDefaults(object):
    '''Common geometry strings, objects and methods for building defaults.

    Allows quick access to default parameters.  It is useful for consistent testing.

    Users can subclass geometric defaults and add specific parameters
    (loading, material, geometric, etc.) to improve start-up and reduce
    redundant code. Here are some objects that can be found in and subclassed
    from this base class:

    - Base : Default geometry strings
    - Base : Default Geometry objects
    - Subclass : Default material and geometric/loading parameters
    - Subclass : Default FeatureInputs

    Defaults are maintained in two dicts:

    - `geo_inputs` : A dict of standard geometry strings and special groups.
    - `Geo_objects` : A dict of converted geo_inputs into Geometry objects.

    Methods
    -------
    get_FeatureInput(Geometry, load_params=None, mat_props=None, **kwargs)
        Return a dict of the basic FeatureInput object; subclass in a model.
    get_materials(mat_props)
        Return a list of materials in order from a mat_props dict or DataFrame.
    generate(selection=None, geo_inputs=False)
        Yield a generator of selected geometries.

    Notes
    -----
    DEV: add entries to the Default dicts.  Removing existing dict entries or
    "trimming" the Default dicts will break tests (not recommended).

    Material properties, geometric/loading parameters and FeatureInput cannot be
    generalized and are thus left to the author to define in their custom defaults
    subclass.  The best place to customize this is in a custom models module.

    Examples
    --------
    Base: Idiomatic instantiation of Base Defaults

    >>> bdft = BaseDefaults()                              # instantiation

    Access a set of built-in geometry strings

    >>> bdft.geos_most                                     # list of geometry Input strings
    [('0-0-2000'), ('1000-0-0'), ('600-0-800'),
     ('500-500-0'), ('400-200-800')]

    Access a set of built-in Geometry objects (converted geometry strings)

    >>> bdft.Geos_simple                                   # list of Geometry objects
    [<Geometry object ('0-0-2000')>,
     <Geometry object ('1000-0-0')>,
     <Geometry object '600-0-800')>,
     <Geometry object ('500-500-0')>,]

    Subclass: Idiomatic import and instantiation of custom Defaults (see Wilson_LT ex.)

    >>> from lamana.models import Wilson_LT as wlt         # user-implemmented Defaults
    >>> dft = wlt.Defaults()                               # subclassed from BaseDefaults

    Access Defaults loading parameters, material properties and FeatureInput

    >>> dft.load_params
    {'R': 12e-3, 'a': 7.5e-3, 'p': 1, 'P_a': 1, 'r': 2e-4}
    >>> dft.mat_props
    {'HA': [5.2e10, 0.25], 'PSu': [2.7e9, 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,
     'Globals': None,}

    Reassign Defaults instances (e.g. R, p)

    >>> dft.load_params = {
    ...     'R': 50e-3, 'a': 7.5e-3, 'p' : 5,
    ...     'P_a': 1, 'r': 2e-4,
    ... }
    >>> dft.load_params
    {'R': 50e-3, 'a': 7.5e-3, 'p' : 5, 'P_a': 1, 'r': 2e-4,}

    '''

    def __init__(self):

        # TODO: Add BaseDefaults attributes to claim the namespace
        # i.e, load_params = None, mat_props = None, FeatureInput = None
        # Consider this architexture rather than leave the author with oneous to def vars

        # Geometry Input Strings
        # DEV: Add geometry strings here.  Do not remove.
        self.geo_inputs = {
            '1-ply': ['0-0-2000', '0-0-1000'],
            '2-ply': ['1000-0-0'],
            '3-ply': ['600-0-800', '600-0-400S'],
            '4-ply': ['500-500-0', '400-[200]-0'],
            '5-ply': ['400-200-800', '400-[200]-800', '400-200-400S'],
            '6-ply': ['400-[100,100]-0', '500-[250,250]-0'],
            '7-ply': ['400-[100,100]-800', '400-[100,100]-400S'],
            '9-ply': ['400-[100,100,100]-800'],
            '10-ply': ['500-[50,50,50,50]-0'],
            '11-ply': ['400-[100,100,100,100]-800'],
            '13-ply': ['400-[100,100,100,100,100]-800'],
        }

        # To add keys, first add logic to automate appending dict_ in groupify
        # This should add a key to geo_inputs.  Geo_objects will auto-mimic this logic.
        # Then define attributes (below) for geo_inputs and Geo_objects for API access.
        self.geo_inputs = self._groupify_dict(self.geo_inputs)     # needed for next line
        self.Geo_objects = self._groupify_dict(self.geo_inputs, Geo_obj=True)

        # ATTRIBUTES ----------------------------------------------------------
        # Quick access to geometry related groups
        # Geometry Input String Attributes
        self.geos_even = self.geo_inputs['even']
        self.geos_odd = self.geo_inputs['odd']
        self.geos_most = self.geo_inputs['most']
        self.geos_special = self.geo_inputs['special']
        self.geos_full = self.geo_inputs['full']
        self.geos_full2 = self.geo_inputs['full2']
        self.geos_full3 = self.geo_inputs['full3']
        self.geos_all = self.geo_inputs['all']
        self.geos_standard = self.geo_inputs['standard']
        self.geos_symmetric = self.geo_inputs['symmetric']
        self.geos_inner_i = self.geo_inputs['inner_i']
        self.geos_general = self.geo_inputs['general conv.']
        self.geos_unconventional = self.geo_inputs['unconventional']
        self.geos_dissimilar = self.geo_inputs['dissimilar']
        self.geos_sample = self.geo_inputs['sample']

        # Geometry Object Attributes
        self.Geos_even = self.Geo_objects['even']
        self.Geos_odd = self.Geo_objects['odd']
        self.Geos_most = self.Geo_objects['most']
        self.Geos_special = self.Geo_objects['special']
        self.Geos_full = self.Geo_objects['full']
        self.Geos_full2 = self.Geo_objects['full2']
        self.Geos_full3 = self.Geo_objects['full3']
        self.Geos_all = self.Geo_objects['all']
        self.Geos_standard = self.Geo_objects['standard']
        self.Geos_symmetric = self.Geo_objects['symmetric']
        self.Geos_inner_i = self.Geo_objects['inner_i']<commit_msg>Add todo to reduce redundancy<commit_after># -----------------------------------------------------------------------------
'''Classes and functions for handling user inputs.'''
# Geometry(): parse user input geometry strings to a tuple of floats (and lists/str)
# BaseDefaults(): library of general geometry defaults; subclassed by the user.
# flake8 input_.py --ignore=E265, E501, N803, N806, N802, N813, E133

import itertools as it
import collections as ct

import pandas as pd

import lamana as la
from lamana.utils import tools as ut

# =============================================================================
# USER INPUT ------------------------------------------------------------------
# =============================================================================
# Parses informations and generates User Input objects, i.e. Geometry()


class Geometry(object):
    '''Parse input geometry string into floats.

    When a single (or a list of) geometry string(s) is passed to the
    `lamana.distributions.Case.apply()` method, this class parses those strings
    into (outer, [inner], middle, 'S') format'; 'S' is optional.

    Here are examples of conventions used to write geometry strings:

    - General: outer-[inner_i]-middle
    - Short-hand: outer-inner-middle

    Formatted in General Convention, a converted namedtuple of the geometry
    is returned.  Examples of GeometryTuples:

    - (400.0, [200.0], 800.0)                              # multi-ply
    - (400.0, [200.0], 800.0, 'S')                         # multi-ply, symmetric
    - (400.0, [100.0, 100.0], 800.0)                       # multi-ply, [inner_i]

    Parameters
    ----------
    geo_input : list or tupled list
        Geometry thicknesses of a laminate.

    Attributes
    ----------
    total
    total_middle
    total_inner
    total_inner_i
    total_outer
    is_symmetric
    namedtuple : namedtuple
        A GeometryTuple in General Convention, e.g. (400.0, [200.0], 800.0, 'S').
    geometry : list
        The converted and parsed geometry string.
    middle : float
        Middle layer thickness.
    inner : list of floats
        Inner layer thicknesses in micrometers.
    outer : float
        Outer layer thickness.
    string : str
        The input geometry string converted to General Convention format.

    Examples
    --------
    >>> g1 = ('0-0-2000')                                  # Monolith
    >>> g2 = ('1000-0-0')                                  # Bilayer
    >>> g3 = ('600-0-800')                                 # Trilayer
    >>> g4 = ('500-500-0')                                 # 4-ply
    >>> g5 = ('400-200-800')                               # Short-hand; <= 5-ply
    >>> g6 = ('400-200-400S')                              # Symmetric
    >>> g7 = ('400-[200]-800')                             # Gen. convention; 5-ply
    >>> g8 = ('400-[100,100]-800')                         # Gen. convention; 7-plys
    >>> la.input_.Geometry(g5)
    Geometry object (400.0-[200.0]-800.0)

    '''

    def __init__(self, geo_input):
        '''Ideally a want to call Geometry and get namedtuple auto; return self?'''

        # TODO: Consolidate into namedtuple or self
        # TODO: rename geometrytuple
        self.namedtuple = self._parse_geometry(geo_input)  # a namedtuple; see collections lib
        self.geometry = self._parse_geometry(geo_input)    # a namedtuple; see collections lib
        self.middle = self.geometry.middle                 # attributes from namedtuple; symmetric sensitive
        self.inner = self.geometry.inner
        self.outer = self.geometry.outer

        self.string = self.__class__._to_gen_convention(geo_input)

        # Private attribute used for set comparisons and hashing
        self._geometry_hash = self._parse_geometry(geo_input, hash_=True)

    def __str__(self):
        '''Returns trimmed geometry string.'''
        return self.string

    def __repr__(self):                                    # for object calls
        '''Returns Geometry object string.'''
        return '{} object ({})'.format(self.__class__.__name__, self.string)

    def __eq__(self, other):
        '''Compare GeometryTuples.'''
        if isinstance(other, self.__class__):
            #print(self.__dict__)
            #print(other.__dict__)
            return self.__dict__ == other.__dict__
        return NotImplemented

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is NotImplemented:
            return result
        return not result

    def __hash__(self):
        '''Allow set comparisons.

        The only required property for a hash is that objects which equally
        compare have the same hash value (REF 035).  `self.__dict__` is unhashable
        due to the inner list (lists are mutable, thus unhashable).  So a copy is
        made called `_geometry_hash` of GeometryTuple where the inner value is
        tupled instead.

        '''
        return hash(self._geometry_hash)
        #return hash(tuple(sorted(self.__dict__.items())))
        #return hash(self._geo_string)

    def _parse_geometry(self, geo_input, hash_=False):
        '''Return a namedtuple of outer-inner-middle geometry values.

        Per General Convention, a GeometryTuple has floats and an inner list.
        Checks for symmetry, handles inner list, then makes the GeometryTuple.

        Also can create a hashable version of GeometryTuple; tuple instead of
        list for inner_i.

        Parameters
        ---------
        geo_input : tupled str or str
            outer-inner-middle values or outer-[inner]-middle values.

        Returns
        -------
        namedtuple of mixed types
            GeometryTuple: numeric values converted to floats; (outer, [inner], middle,'S?')
        '''

        def check_symmetry(last):
            '''Yield float or str if 'S' is in the last token of the geometry string.

            Examples
            --------
            >>> [i for i in check_symmetry('400S')]
            [400.0, 'S']
            >>> [i for i in check_symmetry('400')]
            [400.0]
            '''
            if last.endswith('S'):
                # Converts last '400S' to [400.0,'S']
                #print(type(last))
                number = float(last[:-1])
                letter = last[-1]
                yield number
                yield letter
            else:
                # Converts last '400' to [400.0]
                yield float(last)

        # Inner Parsing
        def parse_inner(inside):
            '''Yield values from the inner list of a geometry string.

            Also parses inner_i (if multiple inners are found) str to floats.
            This is later converted to a list of inner_i, as required by LPEP 001.01.

            Examples
            --------
            >>> list(parse_inner('[100,100]'))
            [100.0, 100.0]
            >>> list(parse_inner('[200]'))
            [200.0]
            >>> list(parse_inner('200'))
            [200.0]

            Raises
            ------
            TypeError
                If a non-string is passed in for the geo_input arg.
            Exception
                If the parsed geo string is less than 3 tokens.

            '''
            if inside.startswith('['):
                if ',' in inside:
                    # Convert inside string '[100,100]' to [100.0,100.0]
                    for item in inside[1:-1].split(','):
                        yield float(item)
                else:
                    # Convert inside string '[200]' to [200.0]
                    converted = inside[1:-1]               # excludes bracket strings
                    #print(converted)
                    yield float(converted)
            else:
                # Convert insde string '200' to [200] if brackets aren't found
                yield float(inside)

        # Convert Input String
        def _make_GeometryTuple(tokens, hashable=False):
            '''Return a namedtuple of floats representing geometry thicknesses.'''
            outer = float(tokens[0])
            inner_hash = tuple(parse_inner(tokens[1]))     # tupled inner for hash()
            inner = list(parse_inner(tokens[1]))           # complies w LPEP 001.01
            middle = tuple(check_symmetry(tokens[-1]))

            '''Should warn the object is symmetric though.'''
            if 'S' not in geo_input:
                GeometryTuple = ct.namedtuple(
                    'GeometryTuple', ['outer', 'inner', 'middle'])
                if hashable:
                    return GeometryTuple(outer, inner_hash, middle[0])
                return GeometryTuple(outer, inner, middle[0])
            else:
                GeometryTuple = ct.namedtuple(
                    'GeometryTuple', ['outer', 'inner', 'middle', 'symmetric'])
                if hashable:
                    return GeometryTuple(outer, inner_hash, middle[0], middle[-1])
                return GeometryTuple(outer, inner, middle[0], middle[-1])

        # Create --------------------------------------------------------------
        '''Replace with try-except block.'''
        # Crude exception handling
        tokens = geo_input.split('-')
        if not isinstance(geo_input, str):
            raise TypeError("Cannot parse input type.  Supported types: str")
        elif len(tokens) < 3:
                # TODO: Replace with custom exception
                raise Exception("Input token is too short. Supported geometry string format: 'outer-[inner_i]-middle'")
        else:
            '''Find another name for hash_ which is a bool'''
            # Gets hash_ bool from parsed_geometry
            return _make_GeometryTuple(tokens, hashable=hash_)

    @classmethod
    def _to_gen_convention(cls, geo_input):
        '''Return a geometry string converted to general convention.'''
        tokens = geo_input.split('-')
        first = tokens[0]
        inside = tokens[1]
        last = tokens[-1]
        #print(inside)

        # Convert strings to general convention
        first = str(float(first))

        if inside.startswith('['):
            # Convert inside string '[100,100]' to '100.0,100.0'
            if ',' in inside:
                converted = [str(float(item)) for item in inside[1:-1].split(',')]
                inside = ','.join(converted)
            else:
                # Convert inside string '[200]' to '200.0'
                inside = str(float(inside[1:-1]))
        elif not inside.startswith('['):
            # Convert inside string '200' to 200.0 if brackets aren't found
            inside = str(float(inside))
        # Always add brackets
        inside = ''.join(['[', inside, ']'])
        #print('Converting geometry string to general convention.')

        if last.endswith('S'):
            last = str(float(last[:-1]))
            last = ''.join([last, 'S'])
        elif not last.endswith('S'):
            last = str(float(last))

        geo_string = '-'.join([first, inside, last])
        return geo_string

    # API --------------------------------------------------------------------
    # DEPRECATED: itotal() in 0.4.3d
    @property
    def total(self):
        '''Calculate total thickness for laminates using any convention.'''
        if self.is_symmetric:
            factor = 2
        else:
            factor = 1
        return 2 * self.outer + 2 * sum(self.inner) + factor * self.middle

    @property
    def total_middle(self):
        '''Calculate total thickness for middle lamina using any convention.'''
        if self.is_symmetric:
            return 2 * self.middle
        else:
            return self.middle

    @property
    def total_inner(self):
        '''Calculate total thickness for inner lamina using any convention.'''
        return 2 * sum(self.inner)

    @property
    def total_inner_i(self):
        '''Calculate total thickness for the ith inner lamina.'''
        result = [inner_i * 2 for inner_i in self.inner]
        return result

    @property
    def total_outer(self):
        '''Calculate total thickness for outer lamina using any convention.'''
        return 2 * self.outer

    @property
    def is_symmetric(self):
        '''Return True if 'S' convention is used.

        Examples
        --------
        >>> g5 = ('400-200-800')                           # Short-hand use; <= 5-ply
        >>> g6 = ('400-200-400S')                          # Symmetric
        >>> for geo in [g5, g6]
        ...     geo.is_symmetric
        False True
        '''
        return 'S' in self.geometry


# -----------------------------------------------------------------------------
# UTILITY
# -----------------------------------------------------------------------------
# Supplies basic dicts and methods by supplying custom case-building information.

class BaseDefaults(object):
    '''Common geometry strings, objects and methods for building defaults.

    Allows quick access to default parameters.  It is useful for consistent testing.

    Users can subclass geometric defaults and add specific parameters
    (loading, material, geometric, etc.) to improve start-up and reduce
    redundant code. Here are some objects that can be found in and subclassed
    from this base class:

    - Base : Default geometry strings
    - Base : Default Geometry objects
    - Subclass : Default material and geometric/loading parameters
    - Subclass : Default FeatureInputs

    Defaults are maintained in two dicts:

    - `geo_inputs` : A dict of standard geometry strings and special groups.
    - `Geo_objects` : A dict of converted geo_inputs into Geometry objects.

    Methods
    -------
    get_FeatureInput(Geometry, load_params=None, mat_props=None, **kwargs)
        Return a dict of the basic FeatureInput object; subclass in a model.
    get_materials(mat_props)
        Return a list of materials in order from a mat_props dict or DataFrame.
    generate(selection=None, geo_inputs=False)
        Yield a generator of selected geometries.

    Notes
    -----
    DEV: add entries to the Default dicts.  Removing existing dict entries or
    "trimming" the Default dicts will break tests (not recommended).

    Material properties, geometric/loading parameters and FeatureInput cannot be
    generalized and are thus left to the author to define in their custom defaults
    subclass.  The best place to customize this is in a custom models module.

    Examples
    --------
    Base: Idiomatic instantiation of Base Defaults

    >>> bdft = BaseDefaults()                              # instantiation

    Access a set of built-in geometry strings

    >>> bdft.geos_most                                     # list of geometry Input strings
    [('0-0-2000'), ('1000-0-0'), ('600-0-800'),
     ('500-500-0'), ('400-200-800')]

    Access a set of built-in Geometry objects (converted geometry strings)

    >>> bdft.Geos_simple                                   # list of Geometry objects
    [<Geometry object ('0-0-2000')>,
     <Geometry object ('1000-0-0')>,
     <Geometry object '600-0-800')>,
     <Geometry object ('500-500-0')>,]

    Subclass: Idiomatic import and instantiation of custom Defaults (see Wilson_LT ex.)

    >>> from lamana.models import Wilson_LT as wlt         # user-implemmented Defaults
    >>> dft = wlt.Defaults()                               # subclassed from BaseDefaults

    Access Defaults loading parameters, material properties and FeatureInput

    >>> dft.load_params
    {'R': 12e-3, 'a': 7.5e-3, 'p': 1, 'P_a': 1, 'r': 2e-4}
    >>> dft.mat_props
    {'HA': [5.2e10, 0.25], 'PSu': [2.7e9, 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,
     'Globals': None,}

    Reassign Defaults instances (e.g. R, p)

    >>> dft.load_params = {
    ...     'R': 50e-3, 'a': 7.5e-3, 'p' : 5,
    ...     'P_a': 1, 'r': 2e-4,
    ... }
    >>> dft.load_params
    {'R': 50e-3, 'a': 7.5e-3, 'p' : 5, 'P_a': 1, 'r': 2e-4,}

    '''

    def __init__(self):

        # TODO: Add BaseDefaults attributes to claim the namespace
        # i.e, load_params = None, mat_props = None, FeatureInput = None
        # Consider this architexture rather than leave the author with oneous to def vars

        # Geometry Input Strings
        # DEV: Add geometry strings here.  Do not remove.
        self.geo_inputs = {
            '1-ply': ['0-0-2000', '0-0-1000'],
            '2-ply': ['1000-0-0'],
            '3-ply': ['600-0-800', '600-0-400S'],
            '4-ply': ['500-500-0', '400-[200]-0'],
            '5-ply': ['400-200-800', '400-[200]-800', '400-200-400S'],
            '6-ply': ['400-[100,100]-0', '500-[250,250]-0'],
            '7-ply': ['400-[100,100]-800', '400-[100,100]-400S'],
            '9-ply': ['400-[100,100,100]-800'],
            '10-ply': ['500-[50,50,50,50]-0'],
            '11-ply': ['400-[100,100,100,100]-800'],
            '13-ply': ['400-[100,100,100,100,100]-800'],
        }

        # To add keys, first add logic to automate appending dict_ in groupify
        # This should add a key to geo_inputs.  Geo_objects will auto-mimic this logic.
        # Then define attributes (below) for geo_inputs and Geo_objects for API access.
        self.geo_inputs = self._groupify_dict(self.geo_inputs)     # needed for next line
        self.Geo_objects = self._groupify_dict(self.geo_inputs, Geo_obj=True)

        # ATTRIBUTES ----------------------------------------------------------
        # Quick access to geometry related groups
        # Geometry Input String Attributes
        self.geos_even = self.geo_inputs['even']
        self.geos_odd = self.geo_inputs['odd']
        self.geos_most = self.geo_inputs['most']
        self.geos_special = self.geo_inputs['special']
        self.geos_full = self.geo_inputs['full']
        self.geos_full2 = self.geo_inputs['full2']
        self.geos_full3 = self.geo_inputs['full3']
        self.geos_all = self.geo_inputs['all']
        self.geos_standard = self.geo_inputs['standard']
        self.geos_symmetric = self.geo_inputs['symmetric']
        self.geos_inner_i = self.geo_inputs['inner_i']
        self.geos_general = self.geo_inputs['general conv.']
        self.geos_unconventional = self.geo_inputs['unconventional']
        self.geos_dissimilar = self.geo_inputs['dissimilar']
        self.geos_sample = self.geo_inputs['sample']

        # TODO: must be way to automate these assignments; reduce redundancy.
        # Geometry Object Attributes
        self.Geos_even = self.Geo_objects['even']
        self.Geos_odd = self.Geo_objects['odd']
        self.Geos_most = self.Geo_objects['most']
        self.Geos_special = self.Geo_objects['special']
        self.Geos_full = self.Geo_objects['full']
        self.Geos_full2 = self.Geo_objects['full2']
        self.Geos_full3 = self.Geo_objects['full3']
        self.Geos_all = self.Geo_objects['all']
        self.Geos_standard = self.Geo_objects['standard']
        self.Geos_symmetric = self.Geo_objects['symmetric']
        self.Geos_inner_i = self.Geo_objects['inner_i']
{'fraction_non_alphanumeric': 0.1455548041408729, 'fraction_numerical': 0.035842480362066276, 'mean_word_length': 1.8510938079347423, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 173, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 22, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5784118', 'n_tokens_mistral': 11700, 'n_tokens_neox': 10299, 'n_words': 3191}