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
orgeos_full
,geos = ['400-[200]-800', '400-[100,100]-400S']
Names referencing ``Geometry`` objects are capatlized:
G
,Geo_objects
,Geos
orGeos_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
. 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:
- instantiated
- user info is applied such as geoemtry strings, model name, etc.
- method and proerties are accessed, such as
plot()
andtotal
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:
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.- ID: variables related to layer and row identifications
layer_
,side_
,matl_
,type_
,t_
- Dimensional: variables of heights relative to cross-sectional
planes
label_
,h_
,d_
,intf_
,k_
,Z_
,z_
- ID: variables related to layer and row identifications
- 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 LaminateModel
s
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
: updatedSnapshot
of IDs and dimensionals.LMFrame
: updated LFrame with models computed columns.
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
. FeatureInput
s
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):
- interfacial - point on unbound outer surfaces and bound internal surfaces.
- internal - point with the lamina thickness between interfaces
- discontinuity - point on bounded interfaces pertaining to an adjacent lamina
- 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).
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).
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.