Solver Benchmark

The problem of solving the panel strengths that satisfy the no-flow-through condition poses a linear system of equation of the form

\[\begin{align*} A y = b ,\end{align*}\]

where $A$ is the matrix containing the geometry of the panels and wake, $y$ is the vector of panel strengths, and $b$ is the vector of boundary conditions. This is trivially solved as

\[\begin{align*} y = A^{-1}b ,\end{align*}\]

however, depending on the size of $A$ (which depends on the number of panels) it can become inefficient or even unfeasible to explicitely calculate the inverse of $A$. Multiple linear solvers are available in FLOWPanel that avoid explicitely inverting $A$, which are described and benchmarked as follows.

The complete code is available at examples/sweptwing_solverbenchmark.jl but you should also be able to copy and paste these lines after running the first section of this example.

Backslash operator \

Most programming languages implement an operator \ that directly calculates the matrix-vector product $A^{-1}b$. This is more efficient than directly inverting $A$ and then multiplying by $b$, without loosing any accuracy. This linear solver is available under this function:

FLOWPanel.solve_backslash!Function
solve_backslash!(y::AbstractVector, A::AbstractMatrix, b::AbstractVector)

Solves a linear system of equations of the form $Ay = b$ using the \ operator.

The solution is stored under y.

source

and is used as follows:

import Printf: @sprintf

function calc_lift_drag(body, b, ar, Vinf, magVinf, rho; verbose=true, lbl="")

    CLexp = 0.238
    CDexp = 0.005

    str = ""

    if verbose
        str *= @sprintf "| %15.15s | %-7s | %-7s |\n" "Solver" "CL" "CD"
        str *= "| --------------: | :-----: | :-----: |\n"
    end

    # Calculate velocity away from the body
    Us = pnl.calcfield_U(body, body; fieldname="Uoff",
                            offset=0.02, characteristiclength=(args...)->b/ar)

    # Calculate surface velocity U_∇μ due to the gradient of the doublet strength
    UDeltaGamma = pnl.calcfield_Ugradmu(body)

    # Add both velocities together
    pnl.addfields(body, "Ugradmu", "Uoff")

    # Calculate pressure coeffiecient
    Cps = pnl.calcfield_Cp(body, magVinf; U_fieldname="Uoff")

    # Calculate the force of each panel
    Fs = pnl.calcfield_F(body, magVinf, rho; U_fieldname="Uoff")
    # Calculate total force of the vehicle decomposed as lfit, drag, and sideslip
    Dhat = Vinf/pnl.norm(Vinf)        # Drag direction
    Shat = [0, 1, 0]              # Span direction
    Lhat = pnl.cross(Dhat, Shat)      # Lift direction

    LDS = pnl.calcfield_LDS(body, Lhat, Dhat)

    L = LDS[:, 1]
    D = LDS[:, 2]

    # Force coefficients
    nondim = 0.5*rho*magVinf^2*b^2/ar   # Normalization factor
    CL = sign(pnl.dot(L, Lhat)) * pnl.norm(L) / nondim
    CD = sign(pnl.dot(D, Dhat)) * pnl.norm(D) / nondim
    err = abs(CL-CLexp)/CLexp

    if verbose
        str *= @sprintf "| %15.15s | %-7.4f | %-7.4f |\n" lbl CL CD
        str *= @sprintf "| %15.15s | %-7s | %-7s |\n" "Experimental" CLexp CDexp
        str *= @sprintf "\n\tCL Error:\t%4.3g﹪\n" err*100
    end

    return CL, CD, str

end


# ----- Linear solver test: backslash \ operator
t = @elapsed pnl.solve(body, Uinfs, Das, Dbs; solver=pnl.solve_backslash!)

CL, CD, str = calc_lift_drag(body, b, ar, Vinf, magVinf, rho; lbl="Backslash")
SolverCLCD
Backslash0.23350.0132
Experimental0.2380.005
CL Error:	1.89﹪
Run time:	0.44 seconds

LU decomposition

Pre-calculating and re-using the LU decomposition of $A$ is advantageous when the linear system needs to be solved for multiple boundary conditions.

FLOWPanel.solve_ludiv!Function
solve_ludiv!(y::AbstractVector,
                A::AbstractMatrix, b::AbstractVector; Alu=nothing)

Solves a linear system of equations of the form $Ay = b$ using the LU decomposition of A provided under Alu and LinearAlgebra.ldiv!. If Alu is not provided, it will be automatically calculated using LinearAlgebra.lu.

This method is useful when the system needs to be solved multiple times for different b vectors since Alu can be precomputed and re-used. We recommend you use FLOWPanel.calc_Alu to compute Alu since this function has been overloaded for Dual and TrackedReal numbers. solve_ludiv! has also been overloaded with ImplicitAD to efficiently differentiate the linear solver as needed.

The solution is stored under y.

import FLOWPanel as pnl

Alu = pnl.calc_Alu(A)
pnl.solve_ludiv!(y, A, b; Alu=Alu)
source

Running the solver:

t = @elapsed pnl.solve(body, Uinfs, Das, Dbs; solver=pnl.solve_ludiv!)

CL, CD, str = calc_lift_drag(body, b, ar, Vinf, magVinf, rho; lbl="LUdiv")
SolverCLCD
LUdiv0.23350.0132
Experimental0.2380.005
CL Error:	1.89﹪
Run time:	0.44 seconds

Iterative Krylov Solver

Iterative Krylov subspace solvers converge to the right solution rather than directly solving the system of equations. This allows the user to trade off accuracy for computational speed by tuning the tolerance of the solver.

The generalized minimal residual (GMRES) method provided by Krylov.jl is available in FLOWPanel.

FLOWPanel.solve_gmres!Function
solve_gmres!(y::AbstractVector,
                A::AbstractMatrix, b::AbstractVector; Avalue=nothing,
                atol=1e-8, rtol=1e-8, optargs...)

Solves a linear system of equations of the form $Ay = b$ through the generalized minimal residual (GMRES) method, which is an iterative method in the Krylov subspace.

This iterative method is more efficient than a direct method (solve_backslack! or solve_ludiv!) when A is larger than 3000x3000 or so. Also, iterative methods can trade off accuracy for speed by lowering the tolerance (atol and rtol). Optional arguments optargs... will be passed to Krylov.gmres.

Differentiating through the solver will require extracting the primal values of A, which can be provided through the argument Avalue (this is calculated automatically if not already provided).

The solution is stored under y.

import FLOWPanel as pnl

Avalue = pnl.calc_Avalue(A)
pnl.solve_gmres!(y, A, b; Avalue=Avalue)
source

Running the solver with tolerance $10^{-8}$:

stats = []                      # Stats of GMRES get stored here

t = @elapsed pnl.solve(body, Uinfs, Das, Dbs;
                        solver = pnl.solve_gmres!,
                        solver_optargs = (atol=1e-8, rtol=1e-8, out=stats))

CL, CD, str = calc_lift_drag(body, b, ar, Vinf, magVinf, rho; lbl="GMRES tol=1e-8")
SolverCLCD
GMRES tol=1e-80.23350.0132
Experimental0.2380.005
CL Error:	1.89﹪

Simple stats
 niter: 286
 solved: true
 inconsistent: false
 residuals: []
 Aresiduals: []
 κ₂(A): []
 status: solution good enough given atol and rtol

Run time:	0.71 seconds

Running the solver with tolerance $10^{-2}$:

stats = []                      # Stats of GMRES get stored here

t = @elapsed pnl.solve(body, Uinfs, Das, Dbs;
                        solver = pnl.solve_gmres!,
                        solver_optargs = (atol=1e-2, rtol=1e-2, out=stats))

CL, CD, str = calc_lift_drag(body, b, ar, Vinf, magVinf, rho; lbl="GMRES tol=1e-2")
SolverCLCD
GMRES tol=1e-20.23310.0129
Experimental0.2380.005
CL Error:	2.06﹪

Simple stats
 niter: 88
 solved: true
 inconsistent: false
 residuals: []
 Aresiduals: []
 κ₂(A): []
 status: solution good enough given atol and rtol

Run time:	0.39 seconds

Running the solver with tolerance $10^{-1}$:

stats = []                      # Stats of GMRES get stored here

t = @elapsed pnl.solve(body, Uinfs, Das, Dbs;
                        solver = pnl.solve_gmres!,
                        solver_optargs = (atol=1e-1, rtol=1e-1, out=stats))

CL, CD, str = calc_lift_drag(body, b, ar, Vinf, magVinf, rho; lbl="GMRES tol=1e-1")
SolverCLCD
GMRES tol=1e-10.26250.0133
Experimental0.2380.005
CL Error:	10.3﹪

Simple stats
 niter: 25
 solved: true
 inconsistent: false
 residuals: []
 Aresiduals: []
 κ₂(A): []
 status: solution good enough given atol and rtol

Run time:	0.29 seconds