Sensitivity Analysis

In order to facilitate gradient-based optimization, all linear solves in GXBeam have been overloaded to support automatic differentiation using the ImplicitAD package. Automatic differentiation using ForwardDiff and ReverseDiff should therefore work with without any major headaches, as long as you initialize the internal storage with the correct type. (e.g. system = DynamicSystem(TF, assembly) where TF is an appropriate floating point type).

For example, consider the Cantilever with a Tip Moment example. Suppose we were interested in the sensitivity of tip x and y-displacement with respect to the nondimensional tip moment $\lambda$ when $\lambda=1$. These sensitivites may be computed as follows:

using GXBeam, LinearAlgebra
import ForwardDiff # for forward-mode automatic differentiation
import ReverseDiff # for reverse-mode automatic differentiation
using BenchmarkTools # for benchmarking function performance

L = 12 # inches
h = w = 1 # inches
E = 30e6 # lb/in^4 Young's Modulus

A = h*w
Iyy = w*h^3/12
Izz = w^3*h/12

# create points
nelem = 16
x = range(0, L, length=nelem+1)
y = zero(x)
z = zero(x)
points = [[x[i],y[i],z[i]] for i = 1:length(x)]

# index of endpoints of each beam element
start = 1:nelem
stop = 2:nelem+1

# compliance matrix for each beam element
compliance = fill(Diagonal([1/(E*A), 0, 0, 0, 1/(E*Iyy), 1/(E*Izz)]), nelem)

# create assembly of interconnected nonlinear beams
assembly = Assembly(points, start, stop, compliance=compliance)

# construct objective function
objfun1 = (p) -> begin

    # non-dimensional tip moment
    λ = p[1]

    # dimensionalized tip moment
    m = pi*E*Iyy/L
    M = λ*m

    # prescribed conditions
    prescribed_conditions = Dict(
        # fixed left side
        1 => PrescribedConditions(ux=0, uy=0, uz=0, theta_x=0, theta_y=0, theta_z=0),
        # moment on right side
        nelem+1 => PrescribedConditions(Mz = M)
    )

    # initialize internal storage with the correct type
    system = StaticSystem(eltype(p), assembly)

    # perform static analysis
    system, state, converged = static_analysis!(system, assembly; prescribed_conditions)

    # return the desired outputs
    return [state.points[end].u[1], state.points[end].u[2]]
end

# compute sensitivities using ForwardDiff with λ = 1.0
@btime ForwardDiff.jacobian(objfun1, [1.0])
2×1 Matrix{Float64}:
 -12.025539968259286
  -7.659921842013467
# compute sensitivities using ReverseDiff with λ = 1.0
@btime ReverseDiff.jacobian(objfun1, [1.0])
2×1 Matrix{Float64}:
 -12.025539968259285
  -7.659921842013468

Advanced users, however, may wish to use overloaded versions of each nonlinear solve in order to further decrease the total computational costs associated with obtaining design sensitivites. Overloading the nonlinear solver also significantly reduces the memory requirements associated with using ReverseDiff. Using these overloads, however, requires that the user provide the parameter function parameters = pfunc(p, t) and associated parameter vector p. As described in the documentation for each analysis type, the pfunc function returns a named tuple which contains updated arguments for the analysis, based on the contents of the parameter vector p and the current time t.

# construct pfunc to overwrite prescribed conditions
pfunc = (p, t) -> begin

    # non-dimensional tip moment
    λ = p[1]

    # dimensionalized tip moment
    m = pi*E*Iyy/L
    M = λ*m

    # create dictionary of prescribed conditions
    prescribed_conditions = Dict(
        # fixed left side
        1 => PrescribedConditions(ux=0, uy=0, uz=0, theta_x=0, theta_y=0, theta_z=0),
        # moment on right side
        nelem+1 => PrescribedConditions(Mz = M)
    )

    # return named tuple with new arguments
    return (; prescribed_conditions=prescribed_conditions)
end

# construct objective function
objfun2 = (p) -> begin

    # perform static analysis
    system, state, converged = static_analysis(assembly; pfunc, p)

    # return the desired outputs
    return [state.points[end].u[1], state.points[end].u[2]]
end

# compute sensitivities using ForwardDiff with λ = 1.0
@btime ForwardDiff.jacobian(objfun2, [1.0])
2×1 Matrix{Float64}:
 -12.025539968209076
  -7.659921841992032
# compute sensitivities using ReverseDiff with λ = 1.0
@btime ReverseDiff.jacobian(objfun2, [1.0])
2×1 Matrix{Float64}:
 -12.025539968209076
  -7.659921841992033

This page was generated using Literate.jl.