LPEP

LamAna Python Enhancement Proposals (LPEP) and Micro PEPs.

See also

The LPEP types, submission and content guidelines closely follows PEP 0001.

Note

Most Active LPEPs include a Next Action Section.

LPEP 001: Implementing Coding and Package Standards

  • Status: Active
  • Type: Standards Track
  • Date: Epoch
  • Current Version: 0.1

Standards

This LPEP preserves best practices, standards or customs for develpopers that maintain code consistency. Tne following micro-PEPs are numerically assigned. New micro-PEPs will be added over time or modified with caution.

  1. A General Convention will be standardized for internal code, such that the inner layer(s) is/are consistently returned as a list of floats i.e. 400.0-[200.0]-800.0 and 400.0-[100.0-100.0]-800.0. This format is used to maintain type checking consistency within the code. External use by the user input is not bound by this restriction however; shorthand notation is fine too, e.g. 400-200-800. Such notation will be internally converted to the General Convention.

  2. Except for user values such as layer thicknesses and total calculations (microns, um), all other internal, dimensional variables will assume SI units (i.e. meters, m). These values will be converted for convenience for the user in the DataFrames, (e.g. millimeters, mm). This PEP is adopted to limit excessive unit conversions within code.

  3. Per PEP 8, semi-private variables are marked with a single preceding underscore, i.e. _check_layer_order(). This style is used to visually indicate internal methods/attributes, not particularly important for the user. Double underscores will only be used (sparingly) to prevent name collisions. Internal hook methods with use both trailing and leading underscores, e.g. _use_model_.

  4. The true lamina thickness value (t_) will remain constant in the DataFrame and not vary with height (d_).

  5. In general, use convenient naming conventions that indicate modules where the objects originates, e.g. FeatureInput object. However, whenever possible, aim to use descriptive names that reduce confusion over convienient names, e.g. LaminateModel object instead of ConstructsTheories object.

  6. For compatibilty checks, run nose 2.x and nose 3.x before commits to target Py3to2 errors in tests, (e.g. dict.values()).

  7. Materials parameters are handled internally as a dict formatted in Standard Form (compatible with pandas DataFrames) , but it is displayed as a DataFrame when the materials attribute is called by the user. The Standard form comprises a dict of materials property dicts. By contrast, a Quick Form is allowed as input by the user, but interally converted to the Standard Form.

    • Quick Form: {Material: [Modulus value, Poissons value], ...}
    • Standard Form: {'Modulus': {'Mat1': value,...},'Poissons': {'Mat1': value, ...}
  8. Internally, middle layers from Geometry return the full thickness, not the symmetric thickness.

  9. Thicknesses will be handled this way.

    • \(t\) is the total laminate thickness
    • \(t_k\) is the thickess at lamina k
    • t_ is the internal variable that refers to true lamina thicknesses.
    • The DataFrame column label \(t(um)\) will refer to lamina thicknesses.
    • h_ is also a lamina thickness, relative to the neutral axis; therefore middle layers (and h_) are symmeric about the neutral axis \(t_{middle} = 2h_{middle}\)
  10. p=2 give the most critical points to calculate - interfacial minima and maxima per layer. Maxima correlate with the ‘interface’ label_ and minima correspond to the ‘discont.’ label_. However, at minimun it is importannt to test with p>=5 to calculate all point types (interfacial, internals and neutural axes) perferably for odd plies.

  11. in geometry strings, the dash character - separates layer types outer-inner-middle. The comma , separates other things, such as similar layer types, such as inner_i -[200,100,300]-. The following is an invalid geomtry string '400-[200-100-300]-800'.

  12. Two main branches will be maintained: “master” and “stable”. “master” will reflect development versions, always ahead of stable releases. “stable” will remain relatively unchanged except for minor point releases to fix bugs.

  13. This package will adopt semantic versioning format (MAJOR.MINOR.PATCH). >- MAJOR version when you make incompatible API changes, >- MINOR version when you add functionality in a backwards-compatible manner, and >- PATCH version when you make backwards-compatible bug fixes.

  14. Package releases pin dependencies to prevent breakage due to dependency patches/updates. This approach assumes the development versions will actively address patches to latest denpendency updates prior to release. User must be aware that installing older versions may downgradetheir current installs.

  15. Use incremented, informative names for tests, e.g. the following says “testing a Case method called “plot” with x feature:

    • test_<class>_mtd_<method name>_<optional feature>#
    • test_<class>_prop_<property name>_<optional feature>#.

    Class tests are ordered as below: - Args: args - Keywords: kw - Attribtutes: attr - Special Methods: spmthd - Methods: mthd - Properties: prop

    Function tests apply similarly, where appropriate. Features are appended and purpose: - test_<func>_<feature 1>_<feature ...>_<purpose>#

LPEP 002: Extending Cases with Patterns

  • Status: Deferred
  • Type: Process
  • Date: October 01, 2015
  • Current Version: 0.4.4b

Motivation

As of 0.4.4b, a Cases object supports a group of cases distinguished by different ps where each case is a set of LaminateModels with some pattern that relates them. For example, an interesting plot might show multiple geometries of:

  • Pattern A: constant total thickness
  • Pattern B: constant midddle thickness

In this example, two cases are represented, each comprising LaminateModels with geometries satisfying a specific pattern. Currently Cases does not support groups of cases distinguished by pattern, but refactoring it thusly should be simple and will be discussed here. Our goal is to extend the Cases class to generate cases that differ by parameters other than p.

Desired Ouptut

To plot both patterns together, we need to feed each case seperately to plotting functons. We need to think of what may differ between cases:

  • p
  • loading parameters
  • material properties
  • different geometries, similar plies
  • number plies (complex to plot simulataneously)
  • orientation (not implemented yet)
  • ...

Given the present conditions, the most simple pattern is determined by geometry. Here are examples of cases to plot with particular patterns of interest.

# Pattern A: Constant Total Thickness
case1.LMs = [<LamAna LaminateModel object (400-200-800) p=5>,
             <LamAna LaminateModel object (350-400-500) p=5>,
             <LamAna LaminateModel object (200-100-1400) p=5>,
            ]

# Pattern B: Constant Middle and Total Thickness
case2.LMs = [<LamAna LaminateModel object (400-200-800) p=5>,
             <LamAna LaminateModel object (300-300-800) p=5>,
             <LamAna LaminateModel object (200-400-800) p=5>,
            ]

Specification

To encapsulate these patterns, we can manually create a dict of keys and case values. Here the keys label each case by the pattern name, which aids in tracking what the cases do. The Cases dict should emulate this modification to support labeling.

cases = {'t_total': case1,
         'mid&t_total': case2,}

Cases would first have to support building different cases given groups of different geometry strings. Perhaps given a dict of geometry strings, the latter object gets automatically created. For example,

patterns = {
    't_total': ['400-200-800', '350-400-500', '200-100-1400'],
    'mid&t_total': ['400-200-800', '300-300-800', '200-400-800'],
}

The question then would be, how to label different ps or combine patterns i.e., t_total and ps. Advanced Cases creation is a project for another time. Meanwhile, this idea of plotting by dicts of this manner will be beta tested.

Next Actions

  • Objective: organize patterns of interest and plot them easily with Case and Cases plot methods.
    • Refactor Case and Cases to handle dicts in for the first arg.
    • Parse keys to serve as label names (priority).
    • Iterate the dict items to detect groups by the comma and generate a caselets for cases, which get plotted as subplots using an instanace of `output_.PanelPlot’

See Also

  • LPEP 003

LPEP 003: A humble case for caselets

  • Status: Replaced
  • Type: Process
  • Date: October 05, 2015, March 15, 2016
  • Current Version: 0.4.4b, 0.4.11

Motivation

By the final implementation of 0.4.4b, each case will generate a plot based on laminate data given loading, material and geometric information. Single plots are created, but subplots are desired also, where data can be compared from different cases in a single figure. This proposal suggests methods for organizing such plotting data by defining a new case-related term, a caselet object and its application to a figure object comprising subplots, based on a [STRIKEOUT:PanelPlot] FigurePlot subclass.

Definitions

  • LaminateModel (LM): an object that combines physical laminate dimensions and laminate theory data, currently in the form of DataFrames.
  • case: a group of LMs; an analytical unit typically sharing similar loading, material and geometric parameters. The final outcome is commonly represented by a matplotlib axes.
  • cases: a group of cases each differentiated by some “pattern” of interest, e.g. p, geometries, etc. (see LPEP 002).
  • caselet: (new) [STRIKEOUT:a sub-unit of a case or cases object. Forms are either a single geometry string, list of geometry strings or list of cases.] The final outcome is strongly associated with data pertaining to a matplotlib axes, or subplot component (not an instance or class). (See LPEP 006 for revised definitions)
  • input: (new) The user arg passed to Case() or Cases().

Types of Inputs

The generation of caselet plots as matplotlib subplots requires us to pass objects into Case(*input*) or Cases(*input*). To pass in caselet data, the input must be a container (e.g. list, tuple, dict, etc.) to encapsulate the objects. The container of any type contain caselets or various types including a string, list or case.

For example, if a list is used, there are at least three options for containing caselets:

  1. A list of geometry strings: type(caselet) == str
  2. A nested list of geometry strings: type(caselet) == list
  3. A list of cases: type(caselet) == <LamAna.distributions.Case object>

If a dict is used to contain caselets, the latter options can substitute as dict values. The keys can be either integers or explict labels.

NOTE: as of 0.4.5, the List will be the default input type of caselets . The dict may or may not be implemented in future versions.


[STRIKEOUT:The following is completed implementation as of v0.4.5.]

Forms of Caselet Inputs

  • Container : list or dict Contains the various types that represent cases
  • Contained : str, list or str, cases (0.4.11.dev0) Input types that represent, user-defined separate cases.

List of Caselets

Here we assume the input container type is a homogenous list of caselets. The caselets can be either geometry strings, lists of geometry strings or cases.

Caselets as geometry strings

(Implemented) The idea behind caselets derives from situations where a user desires to produce a figure of subplots. Each subplot might show a subset of the data involved. The simplest situation is a figure of subplots where each subplot (a caselet) plots a different geometry.

>>> import LamAna as la
>>> from LamAna.models import Wilson_LT as wlt
>>> dft = wlt.Defaults()
>>> input = ['400-200-800', '350-400-500', '200-100-1400']
>>> case = la.distributions.Case(dft.load_params, dft.mat_props)
>>> case.apply(input)

Figure of three subplots with different geoemetries.

.. plot::
        :context: close-figs

        >>> case.plot(separate=True)

Here the Case.plot() method plots each geometry independently in a grid of subplots using a specialseparate keyword. NOTE: Currently this feature uses ``_multiplot()`` to plot multiple subplots. Future implentation should include ``Panelplot`` The Cases class is a more generic way to plot multiple subplots, which does not require a separate keyword and handles other caselet types.

>>> cases = la.distributions.Cases(input)

Figure of three subplots with different geoemetries.

.. plot::
        :context: close-figs

        >>> cases.plot()

Caselets as lists

(Implemented) Another example, if we deisre to build a figure of subplots where each subplot is a subset of a case showing constant total thickness, constant middle thickness, constant outer thickness. We define each subset as a caselet and could plot them each scenario as follows:

>>> import LamAna as la
>>> list_patterns = [
        ['400-200-800', '350-400-500', '200-100-1400'],
        ['400-200-800', '300-300-800', '200-400-800'],
        ['400-200-800', '400-100-1000', '400-300-600']
    ]
>>> cases = la.distributions.Cases(list_patterns)

Figure of three subplots with constant total thickness, middle and outer.

.. plot::
        :context: close-figs

        >>> cases.plot()

Caselets as cases

(Implemented) What if we already have cases? Here is a means of comparing different cases on the same figure.

>>> import LamAna as la
>>> list_caselets = [
        ['400-200-800'],
        ['400-200-800', '400-400-400'],
        ['400-200-800', '400-400-400', '350-400-500']
    ]
>>> case1 = la.distributions.Case(dft.load_params, dft.mat_props)
>>> case2 = la.distributions.Case(dft.load_params, dft.mat_props)
>>> case3 = la.distributions.Case(dft.load_params, dft.mat_props)
>>> case1.apply(list_caselets[0])
>>> case2.apply(list_caselets[1])
>>> case3.apply(list_caselets[2])

>>> list_cases = [case1, case2, case3]
>>> cases = la.distributions.Cases(list_patterns)

Figure of three subplots with constant total thickness and different geometries.

.. plot::
        :context: close-figs

        >>> cases.plot()

The following will not be implemented in v0.4.5.

Dict of Caselets

Key-value pairs as labeled cases.

(NotImplemented) What if we want to compare different cases in a single figure? We can arrange data for each case per subplot. We can abstract the code of such plots into a new class PanelPlot, which handles displaying subplots. Let’s extend Cases to make a PanelPlot by supplying a dict of cases.

>>> dict_patterns = {'HA/PSu': case1,
...                  'mat_X/Y': case2,}
>>> cases = la.distributions.Cases(dict_patterns)

Figure of two subplots with three differnt patterns for two laminates with different materials.

.. plot::
        :context: close-figs

        >>> cases.plot()

Key-value pairs as labeled lists

(NotImplemented) We could explicitly try applying a dict of patterns instead of a list. This inital labeling by keys can help order patterns as well as feed matplotlib for rough plotting titles. Let’s say we have a new case of different materials.

>>> dict_patterns = {
...    't_tot': ['400-200-800', '350-400-500', '200-100-1400'],
...    't&mid': ['400-200-800', '300-300-800', '200-400-800'],
...    't&out': ['400-200-800', '400-100-1000', '400-300-600']
... }
>>> new_matls = {'mat_X': [6e9, 0.30],
...              'mat_Y': [20e9, 0.45]}
>>> cases = la.distributions.Cases(
...     dict_patterns, dft.load_params, new_matls
... )

Figure of three subplots with constant total thickness, middle and outer for different materials.

.. plot::
        :context: close-figs

        >>> cases.plot()

Key-value pairs as numbered lists

(NotImplemented) We can make a caselets in dict form where each key enumerates a list of geometry strings. This idiom is probably the most generic. [STRIKEOUT:This idiom is currently accepted in Cases.plot().] Other idioms may be developed and implemented in future versions.

>>> dict_caselets = {0: ['350-400-500',  '400-200-800', '200-200-1200',
...                      '200-100-1400', '100-100-1600', '100-200-1400',]
...                  1: ['400-550-100', '400-500-200', '400-450-300',
...                      '400-400-400', '400-350-500', '400-300-600'],
...                  2: ['400-400-400', '350-400-500', '300-400-600',
...                      '200-400-700', '200-400-800', '150-400-990'],
...                  3: ['100-700-400', '150-650-400', '200-600-400',
...                      '250-550-400', '300-400-500', '350-450-400'],
...                 }
>>> #dict_patterns == dict_caselets
>>> cases = la.distributions.Cases(dict_caselets)

Figure of four subplots with different caselets.  Here each caselet represents a different case (not always the situation).

.. plot::
        :context: close-figs

        >>> cases.plot()

Specification

Currently, the specification outlined here is to convert a caselet input into a caselet using a conversion function. Implementation of a formal caselet object are subject to future consideration.

The current application is to feed a Cases.plot() method with input which is converted to one of the latter types of caselets. At the moment, type handling for caselets occurs in Cases(). This section proposes that type handling for caselets be implemented in the input_ module instead for general use.

This function will handle processing of various input container types.

def to_caselet(input):
    '''Return a Case obect given an input.

    This function accepts each item of a container and processes them into a Case.

    Parameters
    ----------
    input : str, list (of str), case
        This user input becomes a Case object, representing a caselet - a subcomponent
        of other related cases.

    Notes
    -----
    Uses error handling to convert an input into one of the defined caselet types
    str, list of str or case (see LPEP 003).  These caselets derive from homogenous types.

    Heterogenous caselets are not handled, but may be implemented in the future.


    Raises
    ------
    FormatError
        Only a geometry string, homogenous list of geometry strings or case is accepted.

    Returns
    -------
    Case object
        Integer-case, key-value pairs.

    '''
    try:
        # Assuming a list of geometry strings
        case_ = la.distributions.Case(self.load_params, self.mat_props)
        if unique:
            case_.apply(input, unique=True)
        else:
            case_.apply(input)
        self.caselets = [case_]
        # TODO: Brittle; need more robust try-except
    except(AttributeError, TypeError):             # raised from Geometry._to_gen_convention()
        try:
            # If a list of lists
            flattened_list = list(it.chain(*caselets))
            # lists are needed for Cases to recognize separate caselets
            # automatically makes a unique set
            #print(caselets)
            # TODO: what else is _get_unique doing?
            ##self.caselets = [self._get_unique(flattened_list)]
            #print(self.caselets)
        except(TypeError):
            # if a list of cases, extract LMs, else raise
            flattened_list = [LM.Geometry.string for caselet in caselets
                              for LM in caselet.LMs]
            # list is needed for Cases to recognize as one caselet
            # automatically makes a unique set
            ##self.caselets = [self._get_unique(flattened_list)]
            #print(self.caselets)
     raise FormatError('Caselet type is not accepted.  Must be str, list of strings or case') #?
'''
Need to iterate caselets (lists of objects) to package the order of the data.
Then pass that data into the plot functions.  Plot functions should simply
make an axes for each data unit, then return an ax (for singleplot) or figure
(for multiplot).

1. Case only need a single list of input because it only handles one case/time.
2. Cases takes multiple lists or case objects
   - may require separating a caselet into cases bases on what's given.

A Caselets object should accept either number or inputs.  Should rearrange caselets.
Should return a rearrange caselet input.  If this self is passed in, the order
of cases should be preserved

'''

Next Actions

  • Objective: Make abstract PanelPlot class that accepts dicts of LMs for cases to output figures of caselets or cases.
    • build PanelPlot which wraps matplotlib subplots method.
    • inherit from PanelPlot in Case.plot() or Cases.plot()
    • implement in output_
    • make plots comparing different conditions in the same Case (caselets)
    • [STRIKEOUT:make plots comparing different cases using Cases]
  • Abstract idiom for building caselets accepted in Cases.plot().
  • Implement general caselet converter, error-handler in input_
  • Make a caselets class.
  • Revise LPEP to accept LM or LMs as caselet types; refactor to_caselet to handle these types. See output_._multiplot, which defines caselet differently.

See Also

  • LPEP 002

LPEP 004: Refactoring class Stack

  • Status: Draft
  • Type: Process
  • Date: October 20, 2015, March 17, 2016 (revised)
  • Current Version: 0.4.4b1, 0.4.11

Motivation

Inspired to adhere to classic data structures, we attempt to refactor some classes. The present la.constructs.Stack class is not a true stack. Athough built in a LIFO style, there are no methods for reversing the stack. It may be beneficial to the user to add or delete layers on the fly. Stacks, queues and other data structures have methods for such manipulations. Here are some ideas that entertain this train of thought.

Desired Output

  • Insert and remove any layers
  • Access geometry positions in an index way

Specification

  • Make stacks from deques
  • Extend Stack to interpret from geometry strings also

Examples

>>> LM = la.distributions.Cases('400-200-800').LMs
>>> LM.insert('[:,100]')          # eqv. ':-[:,100]-:'
>>> print(LM.geometry, LM.nplies)
<Geometry object (400-[200,100]-800)>, 7

>>> LM.remove('middle')
>>> print(LM.geometry, LM.nplies)
<Geometry object (400-[200,100]-0)>, 6

>>> LM.remove(['outer', 'inner'])
StackError 'If inner layers are removed, outer layers must exist.'

Next Actions

  • Write specification for using deques
  • Write specification for implementing geo_string interpretation.
  • Develop the idea of duples in tandem

See Also

  • analyze_geostrings(): interpret strings nplies, thickness, order.

LPEP 005: Making Concurrent LaminateModels with the new asyncio

  • Status: Draft
  • Type: Process
  • Date: February 23, 2016
  • Current Version: 0.4.10

Motivation

The idea of concurrency offers a potential option for improving creation of LamAna objects. For instance, if 10 LaminateModels are called to be made, rather then waiting for each object to instantiate serially, it may be better to create them in parallel. This proposal is entertains current object creation using concurrency, and it is adapted from this simple, well written set of examples of coroutines and chained coroutines.

Definitions

  • G : Geometry object
  • FI : FeatureInput object
  • St : Stack
  • Sp : Snapshot
  • L : Laminate
  • LM : LaminateModel

When la.distributions.Case.apply() is called, the get_LaminateModel() function creates a generated list of LaminateModels. A series of objects a created accessing 3 core modules.

\[\big[G_{input\_} \rightarrow FI_{distributions-input\_}\big] \longrightarrow \big[St \rightarrow Sp \rightarrow L \rightarrow LM \big]_{constructs}\]

When apply() is called, it has to wait for other serial processes to finish in a certain order before completing. These characteristics of waiting on ordered processes may qualify the LamAna architecture as a candidate for concurrency features in the new Python 3.5 asyncio module.

Implemented Chained Coroutines

We attempt to apply these concepts to LamAna. A summary of the main coroutine is outlined below.

import asyncio

async def get_LaminateModel(geo_string):
    '''Run set of processes in order to give finally create a LaminateModel from a geo_string.'''
    # conv_geometry converts a geo_string to general convention
    # TODO: include geo_string caching

    # TODO: comvert these objects to coroutines (or keep as generators?)
    G = await = la.input_.Geometry(conv_geomtry)
    FI = await la.input_.BaseDefaults.get_FeatureInput(G, **kwargs)      # rewrite FeatureInput
    St = await la.constructs.Stack(FI)
    Sp = await la.constructs.Snapshot(St)                                # snapshot
    L = await la.constructs.Laminate(Sp)                                 # LFrame
    LM = await la.constructs.Laminate(L)                                 # LMFrame

# The main event loop
event_loop = asyncio.get_event_loop()
for geo_string in geo_strings:                                           # unsure if this would work
    try:
        # Consider alternatives to this default loop
        laminate_model = event_loop.run_until_complete(get_LaminateModel(geo_string))
    finally:
        event_loop.close()

NOTE: It is unclear how to advance the geo_strings iterable object in the default asyncio loops

Vetting

Pros:

  • Possbible concurrency, multitasking of LaminateModel creation
  • Clear, explicit illustration of order

Cons:

  • Limited to Python 3.5

Next Actions

  • Look into advancing iterables in an asynio default loop
  • Use mock objects to test this LPEP as proof of concept

LPEP 006: Defining LamAna Objects

  • Status: Draft
  • Type: Informational
  • Date: March 17, 2016
  • Current Version: 0.4.11

Motivation

This LPEP is written to clarify certain types used within LamAna documentation and codebase.

Definitions

Laminate classifications

  • Symmetric: a laminate with symmetry across the neutral axis.
  • Asymmetric: non-symmetry across the netural axis.

Representations

  • geo_string (g): a geometry string typically formatted to general convention (see LPEP 001)
  • Geo_object (G): a Geometry object, an instance of the Geometry class
  • Geo_orient (GO) (NotImplemented): a GeoOrient object, containing in-plane, directional, ply-angle information

Layer types (lytpe)

  • outer: the top and bottom-most layers
  • inner_i: a list (or string representation) of all inner layer thicknesses; inners refers to a subset of inner_i
  • inner: an internal, non-middle, non-outer layer
  • middle: for odd plies, the center layer; for symmetric laminates, this layers passing through the neutral axis

Geometry string containers

Pythonic objects used to signify groups of layer thickness:

  • list: a pythonic list of inner layers, e.g. [100, 100, 50]. Each entry represents equivalent layer thicknesses for both tensile and compressive sides.
  • token: pertaining to one of the layer types
  • duple (NotImplemented): a tuple of dual layer thicknesses for corresponding (tensile, compressive) layers, e.g. (100,300). Each entry represents a significant thickness of a tensile/compressive side for a given layer type. Zero is also not allowed (0,400). A duple replaces one of the thickness positions in a geometry string. The sum of a duple contributes to the total laminate thickness. By definition, duples are only used to specify asymmetric geometries, therefore repeated values are disallowed e.g. (400,400). Also, since middles are singular constructs, duples are disallowed for middle layers.

Geometry strings

Regular geometry strings: a simple, symmetric stacking sequence of outer, inner_i and middle layers. e.g.

- '400-[200]-800'                                          # simple
- '400-[150,50]-800'                                       # inner_i

These strings follow a simple algorithms for calculating layer thicknesses:


\[t_{total, outer} = 2t_{outer}\]
\[t_{total, inner} = 2t_{inner_i}\]
\[t_{total, inner_{i}} = 2\sum_{i}^m t_{inner}\]

\[t_{total} = 2(t_{outer} + t_{inner_i}) + t_{middle}\]
\[n_{plies} = 2(n_{outer} + n_{inner_i}) + n_{middle}\]

Irregular geometry strings: includes assymmetric laminates; involves

- '(300,100)-[150,50]-800'                                 # outer duple
- '400-[150,(75,50),25]-800'                               # inner duple
- '(300,100)-[150,(75,50),25]-800'                         # outer and inner duple

These strings can follow more complex algorithms for calculating layer thickness. For every \(ith\) item in the list of inner_i and \(jth\) index within an \(i\) (duple or non), where \(m\) is the end of the squence and \(C=1\) for duples and \(C=2\) for non-duples:

\[t_{total, outer} = C\sum_{i}^m\sum_{j}^{m=2} t_{outer}\]
\[t_{total, inner} = C\sum_{j}^{m_j} t_{inner}\]
\[t_{total, inner_i} = C\sum_{i}^m\sum_{j}^{m_j} t_{inner}\]
\[t_{total} = t_{outer} + t_{inner_i} + t_{middle}\]
\[n_{plies} = C_1\sum_{i}^{m_i}\sum_{j}^{m_j} n_{outer} + C_2\sum_{i}^{m_i}\sum_{j}^{m_j} n_{inner} + n_{middle}\]

Data structures

Conceptual structures used to represent groups of data:

  • packet: a user-defined input data, e.g. a list of geometry strings. This group are structured according to some pattern of interest (see LPEP 002). A packet may become processed datasets (LMs) encased within a Case object.
  • packets: a group of packets, units that represent separated cases, e.g. a list of lists comprising geometry strings. These groups are ordered according to desired output.
  • stack: the bottom-to-top (tensile-to-compressive) stacking sequence of laminated layers. Represented as lists, deques or other pertinent data structures. Regular stacks reverse inner_i order post middle layer. Irregular stacks must parse duple indices, tensile and compressive.
    • Regular stack: ‘400-[150,50]-800’ –> [400.0, 150.0, 50.0, 800.0, 50.0, 150.0, 400.0]
    • Irregular stack: ‘400-[(150,50)]-800’ –> [400.0, 150.0, 800.0, 50.0, 400.0]
  • LaminateModel (LM): an object that combines physical laminate dimensions and laminate theory data, currently in the form of DataFrames.
  • case: an analytical unit typically sharing similar loading, material and geometric parameters. This object contains a group of LMs; the final outcome is commonly represented by a matplotlib axes.
  • cases: a group of cases each differentiated by some “pattern” of interest, e.g. p, geometries, etc. (see LPEP 002). The final product is commonly represented as a matplotlib Figure. Each group is a smaller case “caselet”
  • caselet: a samller case that is related to a larger group of cases. This conceptual unit is finally manifested as a matplotlib subplot.

Plotting objects

  • figureplot: a matplotlib Figure with attributes for quick access to plot objects. A base class for setting figure options and appearance, akin to a seaborn FacetGrid
  • singleplot: a single matplotlib axes.
  • multiplot: a figure of singleplots represented in subplots.
  • feature plot: a LamAna plotting object based on a specific feature module e.g. DistribPlot

Examples

Analyzed string Information

Number of plies, total laminate thickness and stacking order
(nplies, t_total, order)

General Convention
'400.0-[100.0,100.0]-800.0'
# (7, 2.0, [400.0,100.0,100.0,800.0,100.0,100.0,400.0])

Duple
'(300.0,100.0)-[(50.0, 150.0),100.0]-800.0
# (7, 1.6, [300.0,50.0,100.0,800.0,100.0,150.0,100.0])

Next Actions

  • Swap definitions of “inner_i” and “inner”.

See Also

  • LPEP 001: General Convention

LPEP 007: Redesigning the distributions Plotting Architecture

  • Status: Draft
  • Type: Process
  • Date: April 05, 2016
  • Current Version: 0.4.11

Motivation

The plotting functions were quickly put together prior to LamAna’s offical release. This original plotting architecture lacks robustness and scalability for future feature modules. The current version of Case.plot() and Cases.plot() methods use non-public functions located the output_ module for plotting single axes figures (“single plots”) and multi-axes figures (“multi plots”). The purpose of this proposal is to lay out a robust, lucid architecture for plotting distributions and future feature module outputs.

Desired Ouptut

...

Definitions

See LPEP 007 for formal definitions.

Basic Plot Options

The following objects associate with lower level matplotlib objects:

  • singleplot, multiplot, figureplot

The following definition pertains to a unique LamAna objects that inherits the latter objects:

  • DistribPlot: a class that handles the output of a distributions plot.

Specification

A DistribPlot should be given LamainateModels. While iterating over LaminateModels, information is extracted (e.g. nplies, p) and axes are generated both combining plotting lines and separating unique laminates under various conditions. THis class should inherit from a base that controls how a figure appears. Through iterating the given argument, this class should determine whether the resulting figure should be a singleplot or multiplot. Here is a sample signature for the Distriplot.

import lamana as la

class _FigurePlot(object):
    '''Return a matplotlib Figure with base control.'''
    def __init__(self):
        self.nrows = 1
        self.ncols = 1

        fig, ax = plt.subplots(self.nrows, self.ncols)
        self = fig
        self.axes = ax
        self.naxes = len(ax)
        self.x_data = fig.axes.Axes[0]
        self.y_data = fig.axes.Axes[1]
        #self.patches = extract_patches()

    def update_figure():
        '''Update figure dimensions.'''
        pass
    pass


class DistribPlot(_FigurePlot):
    '''Return a distributions FigurePlot.

    This class needs to process LaminateModels and honor the user-defined packages.

    Parameters
    ----------
    cases_ : Case or Cases object
        The self object of the Case and Cases classes.  Relies on the pre-ordered
        arrangement of the user-defined, package input.
    kwargs : dict
        Various plotting keywords.

    See Also
    --------
    Entry points
    - lamana.distributions.Case.apply: primarily singleplots unless separated
    - lamana.distributions.Cases: primarily multiplots unless combined

    '''
    def __init__(self, cases_, **kwargs):
        super(DistribPlot, self).__init__(cases_, **kwargs)
        self = self.make_fig(cases_)
        self.packages = cases_.packages                    # NotImplemented


    # Temporary
    # TODO: these plotters need to be abstracted from distributions code.
    def _singleplot(self, case):
        singleplot = la.output_._distribplot(case.LMs)
        return singleplot

    def _multiplot(self, cases):
        multiplot = la.output_._multiplot(cases)
        return multiplot

    def make_fig(self, cases_ordered):
        '''Return a figure given cases data.

        Parameters
        ----------
        cases_ordered : Case- or Cases-like
            Contains data required to generate the plots.  Assumes the cases
            preserve the the user-defined order at onset in the caselet_input.

        '''
        if isinstance(cases_ordered, la.distributions.Case):
            # Give a single Case object
            case = cases_ordered
            fig = plt.figure()
            ax = self._singleplot(case)
            fig.axes.append(ax)
        elif isinstance(cases_ordered, la.distributions.Cases:
            # Give a Cases object
            fig = self._multiplot(cases_ordered)
            #plt.suptitle()
            #plt.legend()
        else:
            raise TypeError(
                'Unknown distributions type was pass into {}.'.format(self.__class__)
            )

        return fig


# Mock Implementations ---------------------

# Handles singleplots from Case
def Case.plot(self, **kwargs):
    return la.output_.DistribPlot(*args, **kwargs)


# Handles multiplots from Cases
def Cases.plot(self, **kwargs):
    return la.output_.DistribPlot(*args, **kwargs)

Examples

Singleplots

>>> case = Case(['400-200-800', '400-400-400'])
>>> singleplot = cases.plot()
<matplotlib Figure>
>>> multiplot.naxes
1

Multiplots

>>> cases = Cases([['400-200-800', '400-400-400'], ['100-100-1600']])
>>> multiplot = cases.plot()
<matplotlib Figure>
>>> multiplot.naxes
2

>>> multiplot.axes
[<maplotlib AxesSupbplot>, <maplotlib AxesSupbplot>]
>>> multiplot.packages                                     # orig. input
[['400-200-800', '400-400-400'], ['100-100-1600']]

Vetting

Next Actions

  • Add more attributes as ideas come about
  • Implement and test in future versions; revise Case, Cases and add Packages

See Also

  • LPEP 002: on patterns
  • LPEP 003: packets; a revised form of caselets
  • LPEP 006: unoffical glossary
  • LPEP: 008 Formalizing Packets

LPEP 008: Formalizing Packets: input data for caselets

  • Status: Draft
  • Type: Process
  • Date: April 07, 2016, (Rev. 04/10/16)
  • Current Version: 0.4.11

Motivation

Multiplots require structured data to display plots correctly. Ulitmately this product requires data that has be validated and organized in a simple and discernable format. The Packets class is in input_ datastructure that attempts to funnel various input type into a simple that that object after processing inputs as follows:

  • validate the geometry strings and input formats
  • reformat geometry data to according to internally-accepted, Generation Conventions.
  • reorder or rearrange data if exceptions are raised
  • analyze geometry data

The user can now focus on arranging the data into analytical sub-groups (caselets). This information new, restructured data is supplied to feature module objects such as Case or Cases that package the data according to the user-defined order. Most importantly, plotting functions can simply iterate over the structured data an output plots that reflect this order.

NOTE: the ``Packets`` object was alpha coded in 0.4.11.dev0

Desired Ouptut

An enumerated dict of packet inputs.

This class should focus on cleaning and organizing the data for a feature module function. Let Case and Cases handle the data.

Definitions/Keywords

Terms such as caselet and packet have been developed during the planning phases of defining and refactoring output_ objects into a logical, scaleable framework. See LPEP 006 for formal definitions.

  • packet, packets, LaminateModel, caselet, case, cases

Specification

The simplest approach to ordering data is to handling all incoming inputs upfront. The packets can them be funneled into a clean, restructured form. As of 0.4.11, we introduced the Packet class, intended to convert packet inputs in said object.

Error handling is important for certain scenarios. For example, given a list of geometry strings, a separate caselet must be generated when:

1. The current nplies does not match the nplies in the current axes
2. Another set of ps is discovered

As Packets must handle such an event by analyzing the raw geometry strings upfront. Packet requirement may vary for different feature modules.

class Packets(object):
    '''Return a Packets object by processing user-defined packet inputs.

    This class is an interface for converting various inputs to a formal datastructure.
    It serves to precess inputs as follows:
    - validate: geo_strings are a valid and interpretible
    - reformat: convert a geo_string to General Convention
    - reorder: split unequal-plied geo_strings in separate caselets (if needed)
    - analyze: assessed for duples (NotImplemented)

    This class also handles unique exceptions to form new, separate packets based on various conditions:

    1. The current nplies does not match the nplies in the current axes
    2. Another set of ps is discovered
    3. ...


    Parameters
    ----------
    packet : list
        User input geometry strings; dictates how caselets are organized.

    Returns
    -------
    dict
        Fast, callable, ordered.  Contains int-caselet input, key-value pairs.

    See Also
    --------
    - LPEP 003: original ideas on caselets

    Notes
    -----
    Due to many container types, this class will be gradually extended:
    - 0.4.12.dev0: supports the list container of str, lists of strs and cases.

    Examples
    --------
    See below.

    '''
    def __init__(self, packets):
        self.packets = self.clean(packets)
        self = to_dict(self.packets)

        self.nplies = None
        self.t_total = None
        self.size = len(self.packets)

    def clean(packets):
        '''Return an analyzed reformatted, validated, orderd list of packets.

        Exploits the fine-grain iteration to extract geo_string data
        via analyze_string().

        '''
        # Handle analyses and converting geo_strings to General Convention

        if self.nplies is None:
            self.nplies = {}
        if self.t_total is None:
            self.t_total = {}

        nplies_last = None
        caselet_conv = []

        for packet in packets:
            caselet_new = []
            for geo_string in packet:
                # Validate
                if is_valid(geo_string):
                    # Reformat: Should raise error if invalid geo_string
                    geo_string_conv = la.input_.to_gen_convention(geo_string)
                    # Analyze: extract geo_string data while in the loop
                    nplies, t_total, _ = la.input_.analyze_geostring(geo_string)

                # Store analyzed data in attributes
                self.nplies.add(nplies)
                self.t_total.add(t_total)

                # Reorder: make new list for unequal nplies
                if nplies != nplies_last:
                    geo_string_conv = list(geo_string_conv)

                caselet_new.append(geo_string_conv)
                nplies_last = nplies

            # Ship for final handling and formatting
            if len(caselet_new) == 1:
                return self._handle_types(caselet_new)
            else:
                packets_conv.append(caselet_new)
            return self._handle_types(packets_conv)

    def _handle_types(self):
        '''Return the accepted packets format given several types.

        As of 0.4.11, the list is the only accpeted objecct container. At this
        entry point, users should not be aware of Case or LaminateModels,
        but they included anyway.

        '''
        # Forward Compatibility -----------------------------------------
        # List of Case objects
        # [case_a, case_b, ...] --> [['geo_str1', 'geo_str2'], ['geo_str1'], ...]


        # A Single Case
        # [case] or case --> ['geo_str1', 'geo_str2', 'geo_str3']


        # List of LaminateModels (LMs)
        # [<LM1>, <LM2>, ...] --> [['geo_str1', 'geo_str2'], ['geo_str1'], ...]


        # A Single LaminateModel (LM)
        # [LM] or LM --> [['geo_str1', 'geo_str2', 'geo_str3'], ...]


        # -----------------------------------------------------------------
        # List of lists or geo_strings
        # [['geo_str1', 'geo_str2'], ['geo_str1'], ...] --> _


        # List of geo_strings
        # ['geo_str1', ...] --> _


        # Single geo_string
        # ['geo_str1'] or 'geo_str1' --> ['geo_str1']


        except(AttributeError) as e:
            raise FormatError(
            'Caselet input () is an unrecognized format.'
            ' Use a list of geo_strings'.format(e)
            )

        pass

    def to_dict(self)
        '''Return an enumerated dict of packets.'''
        dict_ = ct.defaultdict(list)
        for i, caselet in enumerate(self.packets):
            dict_[i] = caselet

        return dict_

    def to_list(self):
        '''Return lists of packets.'''
        pass

    @property
    def info(self):
        '''Return DataFrame of information per caselet.'''
        pass

Examples

Boilerplate

>>> import lamana as la
>>> from lamana.input_ import Packets
>>> from lamana.models import Wilson_LT as wlt
>>> dft = wlt.Defaults()

Usage: A packet --> a future case and a singleplot (one axes)

>>> packet = Packets(['400-200-800', '400-400-400'])
>>> packet
<lamana Packets object, `distribution`, size=1>
>>> packet()
{0:  ['400-200-800', '400-400-400']}
>>> case = la.distributions.Case(dft.load_params, dft.mat_props)
>>> case.apply(packet)
>>> singleplot = case.plot
>>> singleplot.naxes
1

Usage: Packets --> a group of cases (caselets) --> multiplot (n>1 axes)

>>> packets = Packets([['400-200-800', '400-400-400'], ['400-0-1200']])
>>> packets()
{0:  ['400-200-800', '400-400-400'],
 1: ['400-0-1200']}
>>> cases = la.distributions.Cases(packets)            # assumes default parameters and properties
>>> singleplot = case.plot
>>> singleplot.naxes
2

Handling: if unequal plies are found, a new packet is generated automatically

>>> str_packets = [                                    # should be one caselet
... '400-200-800', '400-400-400',                      # but nplies=5
... '400-0-1200'                                       # and nplies=3; cannot plot together
]
>>> packets = Packets(str_packets)
Using default distributions objects.
Unequal nplies found.  Separating...
>>> packets()
{0: ['400-200-800', '400-400-400'],
 1: ['400-0-1200']}
>>> packets.size
2
>>> packets.nplies
[5, 3]                                                  # ordered by input position
>>> packets.info                                        # pandas DataFrame
   nplies  p  contained
0  5       5  '400-200-800', '400-400-400'
1  3       5  '400-0-1200'

Feature: For a distributions `Case` or `Cases` object --> stress distribution

>>> packets = Packets(['400-200-800', '400-400-400'], feature='distributions')
>>> packets
<lamana Packets object `distributions`, size=1>

Feature: For a predictions module object (NotImplemented) --> regression plot

>>> packets = Packets(['400-200-800', '400-400-400'], feature='predictions')
>>> packets
<lamana Packets object `predictions`, size=1>

Feature: For a ratios module (NotImplemented) --> layer group in a ratio plot

>>> packets = Packets(['400-200-800', '400-400-400'], feature='ratios')
>>> packets
<lamana Packets object `ratios`, size=1>

Vetting

Benefits: - This approach handles all analyses, conversions,

validations, and reorderings (e.g. nply separation) of user input data. | - It feeds a consistent form to Case and Cases - Off loads the need to figure out what kind of caselet should be made. - Preprocesses with light, strings and lists. - Can later use in conjunction with a some startup functions e.g. Start to simplify user API. - Handle future input types e.g. GeoOrient object.

Next Actions

  • Develop post 0.4.11.
  • Implement the General Convention strings.
  • Implement the ordering algorithms.
  • Implement the isvalid method.
  • Implement into input_ module; refactor distributions Case and Cases to accept Packets. Remove redundant code.

See Also

  • LPEP 003: original ideas on caselets
  • LPEP 007: plotting redesign

LPEP 009: Revisiting Entry Points

  • Status: Draft
  • Type: Process
  • Date: April 07, 2016
  • Current Version: 0.4.11

Motivation

The user input can be complex and difficult to predict. Additionaly, the user should not be bothered with the following:

  1. Worrying about which type to use as an entry point e.g. Case or Cases
  2. Remembering to apply as in Case.apply
  3. Worrying about particular signatures for each feature module.

As feature modules are added, the entry points to LamAna increase while also broading the signature for caselets. This broadening may become confusing over time. The purpose of this proposal is to mitigate the user responsibility in setting up boilerpoint and focus on analysis.

Desired Ouptut

After supplying caselet information, prompt the user with information it requires per feature module, e.g. load_params or mat_props.

Definitions

Specification

Examples

>>> # Geometries to analyze
>>> caselet = ['400-200-800', '400-400-400']
>>> # What kind of analysis?
>>> la.input_.Start(caselet, feature='distributions')
... Please supply loading paramaters.  Press Enter to use defaults.
... Please supply material properties.  Press Enter to use defaults.
... Please supply laminate theory model.  Press Enter to use defaults.
Using default load_params and mat_props...
Using Wilson_LT model...
Done.
[<lamana Case object size=1, p=5>]

Vetting

Next Actions

  • Design an object that routes user to specific feature module objects and prompts for necessary data.

See Also

LPEP 010: Decoupling LaminateModels from Laminate

  • Status: Draft
  • Type: Standards Track
  • Date: May 30, 2016
  • Current Version: 0.4.11

Motivation

The LaminateModel object is not a class, but it is rather a DataFrame object assigned to an instance attribute of the Laminate class. The implementation was originally intended to reduce class objects creation (reducing memory), encourage simplicity and likely reduce the number of looping operations for populating DataFrame rows and columns. However, this implicit architecture of the clandestine LaminateModels can lead to misunderstanding when trying to track the flow of objects. In addition, during refactoring the theories objects, passing a pure Laminate object into the theories.handshake() has proven is impossible at the moment.

In effort to access separate objects and for clarity, this proposal maps out a plan to decouple LaminateModel from Laminate as a seprate object through subclassing.

Desired Ouptut

A LaminateModel object that inherits from Laminate and Stack.

Specification

  1. Given a FeatureInput, create LaminateModel.
  2. If Exception raised, return a Laminate object.
  3. Update tests expecting Laminate to return LaminateModel
  4. Duplicate _build_laminate to _build_primitive; merge former with Phase 2.

The latter objects should be achieved by extracting Phase 3 _update_calucations into LaminateModel. For cleanup, we can separate object parsing attributes into their associated objects. We can then serially call lower level objects to make the final product.

LaminateModel(Laminate(Stack))

Examples

# Create Separate Objects
>>> S = la.constructs.Stack(FI)
>>> S
<Stack object>
>>> L = la.contstructs.Laminate(FI)
>>> L
<Laminate object 400-[200]-800>
>>>LM = la.constructs.LaminateModel(FI)
>>> LM
<LaminateModel object 400-[200]-800>

>>> # Attribute Inheritance
>>> S_attrs = ['stack_order', 'nplies', 'name', 'alias']
>>> all([hasattr(S, attr) for attr in S_attrs])
True
>>> L_attrs = ['FeatureInput', 'Stack', 'Snapshot', 'LFrame']
>>> all([hasattr(L, attr) for attr in ''.join([L_attrs, S_attrs])
True
>>> LM_attrs = ['LMFrame']
>>> all([hasattr(LM, attr) for attr in ''.join([LM_attrs, L_attrs, S_attrs])
True

Vetting

Next Actions

  • Reduce object recreation; notice a FI is passed to Stack and Laminate.
  • Get image of how objects are passed prior to refactoring.

See Also

  • LPEP 004: Refactoring Stack to optimize object creation
  • LPEP 006: Defining objects