DOI

Installation

Asap.jl is not yet registered. Add via:

using Pkg
Pkg.add(url="https://github.com/keithjlee/Asap")

Citing

When using or extending this software for research purposes, please cite using the following:

Bibtex

@software{lee_2024_10581560,
  author       = {Lee, Keith Janghyun},
  title        = {Asap.jl},
  month        = jan,
  year         = 2024,
  publisher    = {Zenodo},
  version      = {v0.1},
  doi          = {10.5281/zenodo.10581560},
  url          = {https://doi.org/10.5281/zenodo.10581560}
}

Other styles

Or find a pre-written citation in the style of your choice here (see the Citation box on the right side). E.g., for APA:

Lee, K. J. (2024). Asap.jl (v0.1). Zenodo. https://doi.org/10.5281/zenodo.10581560

Asap.jl

Asap is...

  • the anti-SAP (2000)
  • results as Soon As Possible
  • another Structural Analysis Package

Designed first-and-foremost for information-rich data structures and ease of querying, but always with performance in mind.

See also: AsapToolkit, AsapOptim, AsapHarmonics

Overview

The primary information object is the Model data structure, that is constructed from a vector of Nodes, Elements and Loads. Model nodes contain 6 DOF by default; for convenience, a subtype TrussModel (TrussNode, TrussElement) for large truss analysis is also provided. The following provides a quick overview of the general taxonomy and workflow.

Node

Nodes are the sole information holders for FEA: boundary conditions and loads are expressed through nodes, and elemental forces and moments are interpolated from the displacements of end nodes. They are constructed as follows:

node1 = Node(position::Vector{Real}, dofs::Vector{Bool})
node2 = Node(position::Vector{Real}, fixity::Symbol)

node1 is constructed by providing its spatial position in XYZ coordinates and a vector of booleans that denote the activity of each degree of freedom, where true means that the DOF is free to move. The DOFs are in the following order:

  • Translation in X
  • Translation in Y
  • Translation in Z
  • Rotation about X
  • Rotation about Y
  • Rotation about Z

node2 is constructed by providing a position and a common boundary condition symbol. The following are available:

  • :free All degrees of freedom are active
  • :fixed All degrees of freedom are fixed in place
  • :pinned All translational degrees of freedom are fixed
  • :x/y/zfixed All degrees of freedom are active with the exception of x/y/z (choose one axis)
  • :x/y/zfree All degrees of freedom are fixed with the exception of x/y/z

Nodes that are part of the same model should be collected into a single vector:

nodes = [node1, node2]

Element

Elements bridge the gap between nodes and govern the displacement relationships via its material and geometric properties (stiffness). They are defined by its start and end nodes, the cross sectional properties, and the DOF releases.

Material/Section

An element must be assigned a cross section Section that contains all geometric and material information necessary for analysis. A material can be constructed via:

material = Material(E::T, G::T, ρ::T, ν::T) where T <: Real

Where:

  • E: Young's Modulus [Force/Distance^2]
  • G: Shear Modulus [Force/Distance^2]
  • ρ: Weight Density [Force/Distance^3]
  • ν: Poisson's Ration [Unitless]

It is up to you to ensure consistency of units. For convenience, steel material properties are provided in two units as: Steel_Nmm and Steel_kNm.

A cross section includes both material and geometric information, and is constructed by:

section1 = Section(material::Material, A::T, Ix::T, Iy::T, J::T) where T <: Real
section2 = Section(A::T, E::T, G::T, Ix::T, Iy::T, J::T) where T <: Real

Where:

  • material is a Material structure
  • A: cross sectional area [Distance^2]
  • Ix: moment of inertia of primary bending axis [Distance^4]
  • Iy: moment of inertia of secondary (orthogonal) bending axis [Distance^4]

The definition of a Material can be bypassed by explicitly providing material properties via the first method.

An Element can now be created via:

element1a = Element(nodes::Vector{Node}, nodeIndex::Vector{Int64}, section::Section)
element1b = Element(nodes::Vector{Node}, nodeIndex::Vector{Int64}, release::Symbol)

element2a = Element(nodeStart::Node, nodeEnd::Node, section::Section)
element2b = Element(nodeStart::Node, nodeEnd::Node, section::Section, release::Symbol)

The first two methods uses the indices of the start and end node in the previously collected vector of nodes. The second method directly inputs the starting and end nodes. Methods a assumed that the element end DOFs are rigidly coupled to the DOF of the end nodes. Methods b provides option for decoupling a subset of rotational DOFs and one or both ends of the element. The options are:

  • :fixedfixed Default
  • :freefixed Decoupled at the starting node
  • :fixedfree Decoupled at ending node
  • freefree Decoupled at both nodes
  • :joist Decoupled at both nodes with the exception of torsional stiffness

You can also set the angle of roll of the cross section with respect to the element local X axis via:

element.Ψ = pi/3

The angle is in radians, and is set to pi/2 by default: this generally ensures that the local primary bending axis is aligned with the XY plan, IE the element is oriented in the 'correct' way with respect to gravity loading. Once one or more elements are defined, they should also be collected in a vector:

elements = [element1a]

Load

Loads can now be defined via the following options:

NodeForce

Define a point load acting at a node in the global X,Y,Z axes [Force, Force, Force]

NodeForce(node::Node, value::Vector{<:Real})

NodeMoment

Define a moment acting at a node in the global X,Y,Z axes [Force×Distance, Force×Distance, Force×Distance]

NodeMoment(node::Node, value::Vector{<:Real})

LineLoad

Define a uniformly distributed load acting on an element in the global X,Y,Z axes [Force/Distance, Force/Distance, Force/Distance]

LineLoad(element::Element, value::Vector{<:Real})

PointLoad

Define a point load acting on an element at a parameterized (from 0 to 1) distance from the starting node in the global X,Y,Z axes [Force, Force, Force]

PointLoad(element::Element, position::Float64, value::Vector{<:Real})

Define as many loads as you like, and collect them in a vector:

loads = [load1, load2, ...]

Model

A model can now be assembled via:

model = Model(nodes, elements, loads)

The structure can be then be analyzed via:

solve!(model)

The following fields are now populated once solve! is run:

For Nodes

  • node.displacement all displacements for the given node
  • node.reaction the induced external force from fixed DOFs (if applicable)

For Elements

  • element.forces end forces in LOCAL COORDINATE SYSTEM
    • Convert to GCS via element.R' * element.forces

For Model

  • model.u the complete displacement vector of the system
  • model.S the global stiffness vector
  • model.compliance the strain energy of the system

Small example

#define nodes
n1 = Node([0., 0., 0.], :free)
n2 = Node([-240., 0., 0.], :fixed)
n3 = Node([0., -240., 0.], :fixed)
n4 = Node([0., 0., -240.], :fixed)

#collect nodes
nodes = [n1, n2, n3, n4]

#define section/material properties and create a section
E = 29e3
G = 11.5e3 
A = 32.9
Iz = 716.
Iy = 236.
J = 15.1

sec = Section(A, E, G, Iz, Iy, J)

#define elements
e1 = Element(nodes, [2,1], sec)
e2 = Element(nodes, [3,1], sec)
e3 = Element(nodes, [4,1], sec)
e3.Ψ = pi/6

#collect elements
elements = [e1, e2, e3]

#define loads
l1 = LineLoad(e1, [0., -3/12, 0.])
l2 = NodeMoment(n1, [-150. * 12, 0., 150. * 12])

#collect loads
loads = [l1, l2]

#assemble model
model = Model(nodes, elements, loads)

#solve model
solve!(model)

#extract information
@show model.u
@show e1.forces
@show n2.reaction

Truss models

Generally the definition of a truss model is the same as above, but using TrussNode, TrussElement, and TrussModel, with the exception that only NodeForce loads are allowed. Other small details:

  • A reduced-information section can be defined via TrussSection, where only the area A and material stiffness E is required: TrussSection(A::Real, E::Real)
  • There is no end-release option for truss models (they are by definition always :freefree)

2D analysis

A 2D analysis can be performed by simply fixing all nodal DOFs that would enable out-of-plane displacement. This is provided via:

planarize!(model; plane = :XY)

By default, it assumes that the active plane is Global XY. Your options are: :XY, :YZ, :ZX

Advanced/More methods

aSAP provides a comprehensive suite of utility functions and extension to base methods for easy querying.

Passed by reference

Note that all objects and their fields are passed by reference, meaning:

nodes = [node1, ...]
elements = [element1, ...]
loads = [load1, ...]

model = Model(nodes, elements, loads)
solve!(model)

model.elements[1].forces == element1.forces # true

nodeID, elementID

When a model is processed, all nodes, elements, and loads are given an integer identifier node.nodeID, element.elementID, load.loadID in order of their appearance. This becomes critical for BridgeElements (see below).

id

All nodes, elements, and loads have a mutable field called id which is set to nothing by default, but can accept a Symbol type to group objects together.

nodegroup1 = [Node(...) for _ = 1:100]
for node in nodegroup1 node.id = :group1 end

nodegroup2 = [Node(...) for _ = 1:100]
for node in nodegroup2 node.id = :group2 end

nodes = [nodegroup1; nodegroup2]

elementgroup1 = [Element(...) for _ = 1:400]
for element in elementgroup1 element.id = :A end

elementgroup2 = [Element(...) for _ = 1:20]
for element in elementgroup2 element.id = :B end

elements = [elementgroup1; elementgroup2; ...]

verticalLoads = [load1, ...]
for load in verticalLoads load.id = :gravity end

horizontalLoads = [load6, ...]
for load in horizontalLoads load.id = :wind end

loads = [verticalLoads; horizontalLoads]

Model = (nodes, elements, loads)
solve!(model)

Indexing of node/element collections and index searching is possible through IDs:

group1displacements = getproperty.(model.nodes[:group1], :displacement)

BelementIndices = findall(model.elements, :B)

Node utilities

fixnode!

Change the DOF activity of a node to a different common boundary condition:

fixnode!(node, :zfixed)

Element utilities

release!

Change the end coupling releases of an element:

release!(element, :joist)

endpoints

Get the XYZ positions of the start and end points of an element:

pends = endpoints(element)

midpoint

Get the XYZ position of the midpoint of an element:

pmid = midpoint(element)

Model utilities

solve!(model, loadset)

Solve for the displacements of the same model with respect to a different set of loads without reprocessing heavy computations (stiffness matrix assembly):

newloads = [load1, load2, load3, ...]

displacements = solve(model, newloads)

solve!(model, newloads)

solve simply returns the new global displacement vector u, but does not assign new values to loads and elements.

solve! updates displacements, forces, reactions, etc. It treats the new load set as if it were the original set. It replaces model.loads with the new load set.

update_DOF!

Sometimes the activity of your nodes might change under the same loading conditions. Instead of redefining a new model simply:

for node in model.nodes[:groupA]
    fixnode!(node, :xfree)
end

update_DOF!(model)
solve!(model)

Reprocessing

Sometimes, you want to start fresh: clear all post-processed values (displacements, forces), reassemble the stiffness matrix, etc. Simply:

solve!(model; reprocess = true)

connectivity

Extract the connectivity matrix C of a model via:

C = connectivity(model::Model)

C is a [nElements × nNodes] sparse matrix where for a given row (element) -1 is assigned to the column corresponding to the starting node, and 1 is assigned to the ending column.

BridgeElement

Sometimes, it makes sense to define an Element between two other elements rather than between nodes, IE defining a set of joists that bridge between two parallel primary beams. This can be done via the BridgeElement:

BridgeElement(elementStart::Element, posStart::Float64, elementEnd::Element, posEnd::Float64, section::Section, release::Symbol)

The section and release inputs are as before. However, the start and end points are defined by their corresponding Element and the parameterized (0 to 1) position from the starting node of that element. IE:

e1 = Element(...)
e2 = Element(...)

girders = [e1, e2]

joists = [BridgeElement(e1, x, e2, x, section, :joist) for x in range(0.1, 0.9, 5)]
for joist in joists joist.id = :bridge end

elements = [girders; joists]

...

model = Model(nodes, elements, loads)

Generates a new bridge element that spans from the midpoint of e1 to the quarter point of e2.

NOTE:

Since by definition all elements must be defined between nodes, the pre-processing step deconstructs all elements and generates new element use of BridgeElements causes the preprocessing step to generate new nodes and elements for assembly:

  • BridgeElements are not explicitly used in the model: they are converted to Elements
  • new Nodes are created wherever bridges touch an element
  • any element that supports a bridge is deleted and replaced with two new elements that span from the initial start node, the newly created bridge node, and the initial end node

This means that the reliance on pass-by-reference for extracting element-wise properties is no longer advised. However, whenever two new elements are generated from an original element that was bridged, they retain the same element.elementID. IE it is possible to determine which sets of elements are part of the same physical element by their shared elementID.

Note that the original element.id is also transferred to all new sub-elements, allowing them to be easily extracted.

Extensions, Related packages

See AsapToolkit.jl for even more utility and post-processing functions.

See SteelSections.jl for readily accessible Section generation of tabulated AISC steel sections.