In [2]:
# Hidden TimeStamp
import time, datetime
st = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')
print('Last Run: {}'.format(st))
Geometry
classThis class is designed for parsing a user input string. This input string is assumed to be a geometry string and is converted to Geometry object.
lamana.input_.Geometry(<input_str>) --> <Geometry object>
When an input string is formatted, it becomes a geometry string. The accepted format is the General Convention representing characteristic laminae types, i.e. outer-[inner_i]-middle.
A Geometry object
is combining mixed Pythonic types - specifically a namedtuple comprising floats, a list and a string (optional). We summarize below:
'400.0-[200.0]-800.0'
Geometry
class instance e.g. <Geometry object (400-[200]-800)>
Names referencing geometry strings are typically lower-case:
g
, geo_inputs
, geos
or geos_full
, geos = ['400-[200]-800', '400-[100,100]-400S']
Names referencing Geometry
objects are typically capatlized:
G
, Geo_objects
, Geos
or Geos_full
, G = la.input_.Geometry(FeatureInput)
BaseDefaults
classThis 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:
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. These added keys dynamically append values.
Note the naming placement of 's':
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.
BaseDefaults
The remaining defaults such as load_params
, mat_props
and FeatureInput
are specific to experimental setups and cannot be generalized effectively. However, there is a get_FeatureInput()
method to help format this object and defaults to certain values if nonne are provided. Additionally, this class can be subclassed to a custom Defaults
class by the author. This has a number of benefits for storing custom start values. See the Author Documentation for examples of subclassing.
Case
classThe Case
class translates user information into managable, analytical units. A Case
object is:
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, then sets up their associated dicts. Specific geometry strings and a model are applied to the case
instance. 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
classThe 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 motivations and details regarding Cases
.
Principally, the constructs
module builds a LaminateModel
object. Technically a LaminateModel
is a pandas
DataFrames representing a physical laminate with 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. Conveniently, 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.
Before we discuss the laminate structure, here we distinguish two ubiquitous variable categories used internally: Laminate and Model variables. In a full laminate DataFrame, these categories comprise variables that are represented by 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 data supplied by user/author interaction. Groups of rows are colored with alternating red and orange colors to distinguish separate layers.
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.layer_
, side_
, matl_
, type_
, t_
label_
, h_
, d_
, intf_
, k_
, Z_
, z_
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.
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
mat_props
, load_params
]v_eq
, D_11T
, D_12T
, M_r
, M_t
, D_11p
, D_12n
, K_r
, K_t
]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).
LaminateModel
ArchitectureThis section will describe in greater detail how LaminateModel
s are constructed.
When the user calls case.apply()
, a number of objects series of inherited objects are made. The phases for building a LaminateModel
object are outlined below and reflected the architecture of lamana.constructs.LaminateModel
class.
Stack
; a primitive laminate of order layersLaminate
; calculate Laminate dimensional values (LFrame)LaminateModel
; calculate laminate theory Model values (LMFrame)Stack
ClassThe 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 the Stack().StackTuple
method creates a namedtuple of the stack information. This object contains attributes to access the:
order
nplies
name
for the laminate, "4-ply", "5-ply"alias
if any, e.g. "Bilayer", "Trilayer"The stack
attribute accesses a dict of the laminate layers ordered from bottom to top (tensile to compressive layers). Now although Python dicts are unsorted, this particular dict is ordered 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'
Laminate
classThe Laminate
class prepares the geometric calcuations of the laminate.
Laminate
inherits from Stack
and 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.
LaminateModel
LaminateModel
object contains all dimensional information of a physical Laminate
and all theoretical calculations using a laminate theory Model
, e.g. stress/strain.
LaminateModel._build_LMFrame()
calls handshake
and tries to pass in an instance of self (black arrow). The self at this point is a primitive Laminate
DataFrame (an LFrame), which comprises IDs and Dimensional columns only. As a Laminate
, the author of a model has 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>).
These computations update the Laminate DataFrame (Laminate.LFrame
), creating a final LaminateModel
. The complete workflow is summarized below.
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
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_`
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.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._build_LFrame()
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._build_LMFrame()
. 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)
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]
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):
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.
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.
Laminate theory is merged with dimensional data to create a LaminateModel
.
LaminateModel
HandlingFor clarity, an illustration of LaminateModel
handling is shown.
The Laminate
DataFrame (LFrame) is passed from constructs
to theories
. If successful, the LaminateModel
is returned to constructs
; otherwise an exception is caught in constructs
and a ModelError
is raised. Further up, this error initiates a rollback to an LFrame, handle in the Feature module.
In [3]:
"""Image Intramodular component should be updated."""
Out[3]:
See the constructs section for how LaminateModel is made.
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 columns of 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:
- updated Laminate DataFrame with model data columns (a.k.a. `LaminateModel`)
- `FeatureInput` with updated `'Globals'` key.
'Globals'
is a dict of calculated constants, used in exported reports see output_ section.
Post-handshake, the self instance of the LaminateModel
is updated with the new LaminateModel
and FeatureInput
(green arrow). Otherwise exceptions are raised. A commom excpetion are for Laminates with p
=1, which detects an INDET value in middle layers. Handling these exceptions is done in the other modules.
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. It is common for a custom model to be named by the author, suffixed by the characters "_LT
").
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.
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 |
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.