Key Package Components

Core Module: input_

Geometry class

This class is designed for parsing a user input (assumed a geometry string) and converts it into a Geometry object.

LamAna.input_.Geometry(geo_input) --> <Geometry object>

A geometry string is formatted to a General Convention representing characteristic laminae types, i.e. outer-inner_i-middle. A Geometry object is created of mixed Pythonic types - specifically a namedtiple comprising floats, a list and a string (optional).

We distinguish the latter string and coverted object types with the following naming conventions:

  • geometry string: raw string of the laminate geometry, e.g. '400-200-800'
  • Geometry object: Geometry class instance e.g. <Geometry object (400-[200]-800)>

Names referencing geometry strings are lower-case:

  • g, geo_inputs, geos or geos_full,
  • geos = ['400-[200]-800', '400-[100,100]-400S']

Names referencing ``Geometry`` objects are capatlized:

  • G, Geo_objects, Geos or Geos_full,
  • G = la.input_.Geometry(FeatureInput)

BaseDefaults class

This class is essentially a storage for common geometry strings and Geometry objects. Placing them here enables simple inheritance of starter objects when using the API.

There are two main dicts which are stored as instance attributes: geo_inputs and Geo_objects

geo_inputs

This is a simple dict of common geometry strings with keys named by the number of plies. Again the number of plies is determined by

\[2(outer + inner) + middle\]

. Here is an example geo_inputs dict:

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'],
}

Additional keys are added to this dict such as ‘geos_even’, ‘geos_odd’ and ‘geos_all’ which create new key-value pairs of groups for even, odd and all geometry strings. Notice the naming placement of ‘s’: “geo_inputs” is the base dict while “geos_” is a grouping of existing dict values appended to the dict. Therefore an author or developer could extend either the base or appended dict items.

Geo_objects

This is a lazy dict. All entries of geo_inputs are automatically converted and stored as Geometry objects. The purpose here is to eliminate the added step of calling Geometry to convert strings. Both this dict and the geo_inputs dict are created using similar private methods, so there mechanisms are parallel.

Subclassing

The remaining defeaults such as load_params, mat_props and FeatureInput are specific to experimental setups and cannot be generalized effectively. However, this class can be subclassed into a custom Defaults class by the author. See the Authour Documentation for examples of subclassing.

Important

DEV: Only add geometry strings to geo_inputs. Removing or “trimming” these dicts may break tests.

Important

In future versions, load_params, mat_props and FeatureInput will be added to BaseDefaults() as attributes to partake in inheritance.

Feature Module: distributions

Case class

The Case class translates user information into managable, analytical units. A Case object is:

  1. instantiated
  2. user info is applied such as geoemtry strings, model name, etc.
  3. method and proerties are accessed, such as plot() and total

Here is an idiomatic example of the latter characteristics:

case = la.distributions.Case(load_params, mat_props)
case.apply(geo_strings=None, model='Wilson_LT', **kwargs)
case.plot(**kwargs)

The case instance accepts loading and material information and sets up their associated dicts. Specific geometry strings and one model is applied to the case object. This apply() method generates LaminateModel objects (FeatureInput objects are also made). Information is parsed, calculated (such as layer thicknesses) and stored in attributes. These attributes and methods are then accessible for performing analysis, most importantly the plot() method.

Therefore, you can think of a case as an analytical unit comprising start up data converted to LaminateModel objects.

Cases class

The Cases class supplies options for manipulating multiple case objects. For example, set operations can be performed on multiple cases. In this context, each case is termed a caselet and typically correlated with a matplotlib subplot. Here is an idiomatic example:

import lamana as la

bdft = la.input_.BaseDefaults()
cases = Cases(bdft.geo_inputs['geos_all'], ps=[2,3,4])

The latter code builds cases for all geometry strings contained in the BaseDefaults() class, one for each p number of datapoints. Therefore in this example dozens of analytical units are built with only three lines of code. See LPEP 002 and LPEP 003 for the motivation and details on Cases.

Core Module: constructs

Principally, the constructs module builds a LaminateModel object. Technically a LaminateModel is a `pandas <http://pandas.pydata.org/>`__ DataFrames representing a physical laminate with a few helpful attributes. DataFrames were chosen as the backend object because they allow for powerful data manipulation analyses and database/spreadsheet-like visualizations with simple methods.

Additionally, the constructs module computes laminate dimensional columns and compiles theoretical calculations handled by the complementary theories module. Conventiently, all of this data is contained in tabular form within the DataFrame. The column names are closely related to computational variables defined in the next sub-section.

Variable Classifications

Before we discuss the Laminate structure, here we distinguish two ubiquitous variable categories used internally: “Laminate” and “model” variables. In in a full laminate DataFrame, these categories comprise variables that are represented as columns. The categories variables, columns and corresponding modules are illustrated in the image below and described in greater detail:

dataframe output

dataframe output

An image of the output for a DataFrame and their labeled categories of columns (IDs, dimensionals and models). The first two categories are computed by constructs classess; the models columns are computed by theories classes and models. The highlighted blue text indicates user interaction. Groups of rows are colored with alternating red and orange colors to distinguish separate layers.

What distinguishes “Laminate” variables from “Model” variables

  • Laminate (or constructs) variables are responsible for building the laminate stack and defining dimensions of the laminate. Internally, these varibles will be semantically distinguished with one trailing underscore.
    1. ID: variables related to layer and row identifications
      1. layer_, side_, matl_, type_, t_
    2. Dimensional: variables of heights relative to cross-sectional planes
      1. label_, h_, d_, intf_, k_, Z_, z_
  • Model (or theories) variables: all remaining variables are relevant for LT calculations and defined from a given model. Since these variables are model-specific, theres is no particular semantic or naming format.

The finer granularity seen with model variables is not essential for typcial API use, but may be helpful when authoring custom code that integrates with LamAna.

Further Details of Model Variables

For more detailed discussions, model variables can be further divided into sub-categories. There common subsets are as follows:

1. **User**: global variables delibrately set by the user at startup
2. **Inline**: variables used per lamina at a kth level (row)
3. **Global**: variables applied to the laminate, accessible by ks

Although model variables are often particular to a chosen model, e.g Wilson_LT, there are some general trends that may be adopted. Some model variables are provided at startup by the user (user_vars). Some variables are calculated for each row of the data within the table (inline_vars). Some variables are calculated by the designated laminate theory model, which provide constants for remaining calculations (global_vars). Global values would display as the same number for every row. These constants are thus removed from the DataFrame, but they are stored internally within a dict. The details of this storage are coded within each model module.

Global values are of particular importance to FeatureInput objects and when exporting meta data as dashboards in spreadsheets. In contrast, Inline values alter directly with the dimensional values thoroughout the lamainate thickness. Names of common variables used in distributions are organized below:

Model Variable Subsets

Model_vars = {user_vars, inline_vars, global_vars}

Examples of Subsets of Model Variables

  • user_vars = [mat_props, load_params]
  • global_vars = [v_eq, D_11T, D_12T, M_r, M_t, D_11p, D_12n, K_r, K_t]
  • inline_vars = [Q11, Q12, D11, D12, strain_r, strain_t, stress_r, stress_t, stress_f]

TIP: Aside from user variables, all others are found as headers for columns in a DataFrame (or spreadsheet).

The Laminate Architecture

This section will describe in greater detail how LaminateModels are constructed.

When the user calls case.apply(), a number of objects are created. We begin with a primitive Stack, which comprises skeletal components for building a Laminate DataFrame (also interally called an LFrame). The phases for building a LaminateModel object are outlined below and outline the architecture of constructs.Laminate class.

  • Phase 1: build a primitive laminate (Stack)
  • Phase 2: calculate Laminate dimensional values (LFrame)
  • Phase 3: calculate laminate theory Model values (LMFrame aka LaminateModel)

Phase 1: The Stack Class

The purpose of the Stack class is to build a skeletal, precusor of a primitive Laminate object. This class houses methods for parsing Geometry objects, ordering layers, adding materials labels for each layer and setting expected stress states for each tensile or compressive side. Stack returns a namedtuple containing stack-related information (described below).

For a given Geometry object instance (commonlly assigned to a capital “G”) the Stack().StackTuple method creates a namedtuple of the stack information. This object contains attributes to access the:

  • stack order
  • the number of plies, nplies
  • the technical name for the laminate, “4-ply”, “5-ply”
  • a convenient alias if any, e.g. “Bilayer”, “Trilayer”

The stack attribute accesses a dict of the laminate layers ordered from bottom to top. Now although Python dicts are unsorted, this particular dict is sorted because each layer is enumerated and stored as keys to perserve the order, layer thickness and layer type (sometimes referred as “ltype”).

Examples
--------
>>> import LamAna as la
>>> G = la.input_.Geometry(['400-200-800'])
>>> G
<Geometry object (400-[200]-800)>

Create a StackTuple and access its attributes
>>> st = constructs.Stack(G).StackTuple    # converts G to a namedtuple
>>> st.order                               # access namedtuple attributes
{1: [400.0, 'outer'],
 2: [200.0, 'inner']
 3: [800.0, 'middle']
 4: [200.0, 'inner']
 5: [400.0, 'outer']}
>>> st.nplies
5
>>> st.name
'5-ply'
>>> st.alias
'standard'

Phase 2: The Laminate class

The Laminate class simply builds a LaminateModel - an object containing all dimensional information of a physical Laminate and all theoretical calculations using a laminate theory Model, e.g. stress/strain.

The Laminate class builds an LFrame object based on the skeletal layout of a stack parsed by and returned from the Stack class. A Geometry object, material parameters and geometric parameters are all passed from the user in as a single FeatureInput object - a dict of useful information that is passed between modules. See *More on ``FeatureInput``* for details. Stack information is stored in an instance attribute called Snapshot and then converted to a set of DataFrames.

Therefore, the IDs and dimensional data are determined and computed by Stack and Laminate. Combined, this information builds an LFrame.

Phase 3: The Laminate class (continued)

Laminate then calls the theories module which “handshakes” between the Laminate module and the custom module containing code of a user-specified, theoretical LT model. It is common for a custom model to be named by the author, suffixed by the characters “_LT”). These computations update the Laminate DataFrame (Laminate.LFrame), creating a final LaminateModel (Laminate.LMFrame). The complete workflow is summarized below.


Summary of LaminateModel Workflow

constructs :: class Stack --> class Laminate

theories :: class BaseModel

Laminate object + “Model” object –> LaminateModel object

Detailed workflow of constructs-theories interaction:

class Stack --> StackTuple
 |
class Laminate --> Snapshot, LFrame, LMFrame
 |
 | # Phase 1 : Instantiate; Determine Laminate ID Values
 | Laminate._build_snapshot(stack) --> Snapshot
 |     |
 |   Stack.add_materials(stack)
 |   Stack.stack_to_df(stack)        # first creation of the Laminate df
 |   Laminate._set_stresses(stack)
 |
 | Laminate._build_laminate(snapshot) --> LFrame
 |
 | # Phase 2 : Calculate Laminate Dimensional Values
 | Laminate._update_columns._update_dimensions() --> LFrame (updated)
 |        label_, h_, d_, intf_, k_, z_, Z_
 |
 | # Phase 3 : Calculate Model Values
 | Laminate._update_columns._update_calculations() --> LMFrame
 |    theories.Model(Laminate)
 |    models.<selected model>
 |       _calc_stiffness()
 |       _calc_bending()
 |       _calc_moment()
 |       global_vars = [`v_eq`, `D_11T`, `D_12T`, ...]
 |       inline_vars = [`Q11`, `D11` `strain_r`, ...]
 |
LaminateModel : df

Additional Details

More on Material Stacking Order

The material order is initially defined by the user mat_props dict in distributions and automatically parsed in the input_ module. Extracting order from a dict is not trivial, so the default sorting is alphabetical order. This order is handled by converting the dict to a pandas index. See Stack.add_materials() method for more details.

As of 0.4.3d4, the user can partially override the default ordering by setting the materials property in the Case instance. This allows simple control of the stacking order in the final laminate stack and Laminate objects. At the moment, a list of materials is cycled through; more customizations have not been implemented yet.

>>> case.material
['HA', 'PSu']                                     # alphabetical order
>>> case.material = ['PSu', 'HA']                 # overriding order
>>> case.material
['PSu', 'HA']
>>> case.apply(...)
<materials DataFrame>                             # cycles the stacking order

More on Laminate

Using Laminate._build_snapshot(), the instance stack dict is converted to a DataFrame (Snapshot), giving a primitive view of the laminate geometry, idenfiers (IDs) and stacking order. This “snapshot” has the following ID columns of infornation, which are accessible to the user in a Case instance (see distributions.Case.snapshot):

Variables addressed: `layer_, matl_, type_, t_`

From this snapshot, the DataFrame can is updated with new information. For example, the sides on which to expected tensile and compressive stresses are located (side_) are assigned to a laminate through the Laminate._set_stresses() method. This function accounts for DataFrames with even and odd rows. For odd rows, ‘None’ is assigned to the neutral axis, implying “no stress”.

Variables addressed: `side_`

Note

This stress assignment is a general designation, coarsely determined by which side of the netural axis a row is found. The rigorous or finite stress state must be calculated through other analytical tools means such as Finite Element Analysis.

Likewise, the DataFrame is further updated with columns of dimensional data (from Dimensional variables) and laminate theory data (from model variables). The current LaminateModel object is made by calling Laminate._update_columns._build_laminates() which updates the snapshot columns to build two DataFrame objects:

Here are similarities between the laminate data columns and the its objects:

  • Snapshot: primiate DataFrame of the Stack (see materials, layer info order).
  • LFrame: updated Snapshot of IDs and dimensionals.
  • LMFrame: updated LFrame with models computed columns.
laminate objects

laminate objects

LMFrame is the paramount data structure of interest containing all IDs, Dimensional and Model variables and p number of rows pertaining to data points within a given lamina.

Dimensional variable columns are populated through the Laminate._update_columns._update_dimensions() method, which contains algorithms for calculating realative and absolute heights, thicknesses and midplane distances relative to the neutral axis. These columns contain dimensional data that are determined independent from the laminate theory model.

Variables addresed: `label_, h_, d_, intf_, k_, Z_, z_`

These variables are defined in the Laminate class docstring. See More on label_ to understand the role of points, p and their relationship to DataFrame rows.

Finally Data variable columns are populated using Laminate._update_columns._update_calculations(). These columnns contain data based on calculations from laminate theory for a selected model. Here global_vars and inline_vars are calculated.

Variables addressed:
--------------------
global_vars = [`v_eq, D_11T, D_12T, M_r, M_t, D_11p, D_12n, K_r, K_t`] --> FeatureInput['Global'] (dict entry)

inline_vars = [`Q11, Q12, D11, D12, strain_r, strain_t, stress_r, stress_t, stress_f`] --> LaminateModel object (DataFrame)

More on FeatureInput

A Feature module defines a FeatureInput object.

For distributions, it is defined in Case. FeatureInputs contain information that is passed between objects. For instance, this object transfers user input data in distributions (converted in input_) to the constructs module to build the laminate stack and populate ID and dimensional columns. A FeatureInput from distributions looks like the following (as of 0.4.4b).

FeatureInput = {
    'Geometry': <Geometry object>,
    'Loading': <load_params dict>,
    'Materials': <mat_props dict>,
    'Custom': <undefined>,
    'Model': <string>,
    'Globals': <dict>,
}

After calculating model data, the “Globals” key is updated containing all necessary globabl_vars. These variables are constant and are necessary for further calculations of inline_vars. Here is an example of Global variables key-value pair in FeatureInput.

FeatureInput['Globals'] = [v_eq, D_11T, D_12T, M_r, M_t, D_11p, D_12n, K_r, K_t]

More on label_

See LPEP 001.02 for standards of API units.

For this explanation, imagine we transverse the absolute height of the laminate at different cross-sectional planes. The values of inline stress points are calculated along different planes throughout the laminate thickness. What happens at interfaces where two materials meet with different stresses? How are these two stress points differentiated in a DataFrame or in a plot? For plotting purposes, we need to define diferent types of points. Here we define some rulse and four types of points found within a (i.e. DataFrame rows):

  1. interfacial - point on unbound outer surfaces and bound internal surfaces.
  2. internal - point with the lamina thickness between interfaces
  3. discontinuity - point on bounded interfaces pertaining to an adjacent lamina
  4. neutralaxis - the middle, symmetric axial plane

How these points are distributed depends on their locations within each lamina and whether they are located on the tensile or compressive side_. The neutral axis exists in physical laminates, but they are only represented as a row in DataFrames of odd ply, odd p laminates; they are not displayed in even laminates. The image below illustrates the different points from above with respect to k_ (the fractional height for a given layer).

points

points

Notice various layers have different point types.

  • Middle layers have two interfacial points, no discontinuities and a neutral axis.
  • All other layers have one interfacial point with a discontinuity if p >= 2.
  • All layers may (or may not) have internal points.
  • Monoliths do not have discontinuities

Note

Only the interfacial points can be theoreticlly verified, representing the maximum principal strains and stresses. The internal and discontinuity points are merely used by matplotlib to connect the points, assuming a linear stress distribution.

Note

The midplane z height (z_) for discontinuities assumes a non-zero, lower limit value equal to the Z_ height of the bounding layer. This value should be verified.

More on IndeterminateError

An IndeterminateError is thrown in cases where values cannot be calculated. An INDET keyword is given as values in DataFrame cells. An example for such an error is determining the stress state side_ for a monolith with one data point (nplies=1, p=1). From a design perspective, the location of the point is ambiguous, either one one interface, but more intuitively at the neutral access. At such a position, the value of stress would report zero, which is misleading for the true stress state of the monolith. Therefore, the InderminateError is thrown, recommending at least p = 2 for disambiguated stress calculations.

Core Module: theories

Laminate theory is merged with dimensional data to create a LaminateModel.

LaminateModel Handling

For clarify, an illustration of LaminateModel handling is shown below.

The Laminate DataFrame (LFrame) is passed from constructs to theories. If successful the LaminateModel is returned to constructs; otherwise an exception is thrown, consumed and the Laminate is returned unchanged (LFrame).

theories flowchart

theories flowchart

Note

The term repr for <LaminateModel object> remains constant refering to a post-theories operation, whether LMFrame is updated with Model columns or not.

When Laminate._update_columns._update_calculations() (represented as Laminate.foo()) is called, an instance of the Laminate self (shown as “x”) is passed to theories.handshake() (black arrow). This function handles all updates to the primitive Laminate DataFrame (LFrame) which comprise IDs and Dimensional columns only. The Laminate gives the models author full access to its attributes. From here, theories.handshakes() searches within the models directory for a model (grey, dashed arrows) specified by the user at the time of instantiation, i.e. Case.apply(*args, model=<model_name>).

A model is simply a module containing code that handles laminate theory calculations. The purpose of the model is to update the primitive LFame with LT calculations. handshake() automatically distinguishes whether the author implemented a class-style or function-style model. The most important hook method/function is ``_use_model_()``, which must be present somewhere inside the model module and must return a tuple containing:

- the updated Laminate DataFrame with model data columns (a.k.a. `LaminateModel`)
- the `FeatureInput` with updated `'Globals'` key.  `'Globals'` is a dict of calculated constants, used in exported reports (see output_ section).

Finally, the Laminate.LMFrame attribute is updated with the new LaminateModel and FeatureInput (green arrow). However, if exceptions are raised, Laminate._update_calculations() handles them by reverting the LMFrame to a copy of LFrame, printing a warning and printing a minor traceback informing the author to refactor the code. This is commom for Laminates with p=1, which detects an INDET in middle layers and must revert to LFrame. The handshake() method for more details on Exceptions.

Custom Models

Sometimes Classical Laminate Theory needs to be modified to fit a specific set of constraints or boundary conditions. The LamAna package has powerful, extensible options for integrating user user-defined (authored) implementations of their own custom laminate theory models.

A library of these custom models, tests and pre-defined defaults are stored in the models directory (sub-package). Code for calculations, related exceptions, FeatureInput variables and defaults are stored in a Models module. theories then merges the model calculations with the passed in Laminate to calculate data columns in the LaminateModel object. This exchange is possbile since the theories module “handshakes” with the constructs module, and the selected model from the models sub-package.

Core Module: output_

A summary of output objects

Object Purpose
SinglePlot Stress distribution for a single geometry
MultiPlot Stress distributions for a multiple geometries
HalfPlot Partial plot of either compression or tension side
QuarterPlot Partial halfplot excluding side without data
PanelPlot A series of subplots side-by-side
RatioPlot Ratio thickness plot; prinicipal stress vs. ratio
PredictPlot Plot of experimental failure load or stress vs. middle layer princ. stress

Note

Development is beta for this module, therefore these objects are not yet implemented. The majority of plotting options are handled by temporary private functions called _distribplot() and _multiplot().

The utils.tools.export() function is used to save regular or temporary file versions of .xslx or .csv files. Files are automatically stored in the default export folder. More details are shown in the Demonstrations file.