Asap.jl is not yet registered. Add via:

using Pkg


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


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

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.


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


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.


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]


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.


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


  • 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


  • 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]


Loads can now be defined via the following options:


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

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


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})


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})


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, ...]


A model can now be assembled via:

model = Model(nodes, elements, loads)

The structure can be then be analyzed via:


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

#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)

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).


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 = :group1 end

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

nodes = [nodegroup1; nodegroup2]

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

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

elements = [elementgroup1; elementgroup2; ...]

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

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

loads = [verticalLoads; horizontalLoads]

Model = (nodes, elements, loads)

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


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

fixnode!(node, :zfixed)

Element utilities


Change the end coupling releases of an element:

release!(element, :joist)


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

pends = endpoints(element)


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.


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)



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

solve!(model; reprocess = true)


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.


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 = :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.


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 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.