Quick Start

The following is a basic tutorial on how to set up and run an analysis of a ducted fan in DuctAPE.

We begin by loading the package:

using DuctAPE

Assemble Inputs

The next step is to create the input object of type DuctedRotor, which contains duct and center body coordinates, rotor inputs, as well as several constants related to paneling the geometry.

Body Geometry

We begin by defining a matrix of coordinates for the duct and another for the center body geometries.

duct_coordinates = [
    0.304466  0.158439
    0.294972  0.158441
    0.28113   0.158423
    0.266505  0.158365
    0.251898  0.158254
    0.237332  0.158088
    0.222751  0.157864
    0.208123  0.157586
    0.193399  0.157258
    0.178507  0.156897
    0.16349   0.156523
    0.148679  0.156177
    0.134222  0.155902
    0.12      0.155721
    0.106044  0.155585
    0.092531  0.155498
    0.079836  0.155546
    0.067995  0.155792
    0.057025  0.156294
    0.046983  0.157103
    0.037937  0.158256
    0.029956  0.159771
    0.02311   0.161648
    0.017419  0.163862
    0.012842  0.166404
    0.009324  0.169289
    0.006854  0.172546
    0.005484  0.176154
    0.005242  0.180005
    0.006112  0.184067
    0.00809   0.188086
    0.011135  0.192004
    0.015227  0.19579
    0.020339  0.199393
    0.026403  0.202735
    0.033312  0.205736
    0.040949  0.208332
    0.049193  0.210487
    0.057935  0.212174
    0.067113  0.21339
    0.076647  0.214136
    0.086499  0.214421
    0.09661   0.214255
    0.10695   0.213649
    0.117508  0.212618
    0.12838   0.211153
    0.139859  0.209267
    0.151644  0.207051
    0.163586  0.204547
    0.175647  0.201771
    0.187807  0.198746
    0.20002   0.19549
    0.212269  0.192017
    0.224549  0.188335
    0.236794  0.18447
    0.249026  0.180416
    0.261206  0.176188
    0.273301  0.171796
    0.28524   0.16727
    0.29644   0.162842
    0.304542  0.159526
]
center_body_coordinates = [
    0.0       0.0
    0.000586  0.005293
    0.002179  0.010047
    0.004736  0.014551
    0.008231  0.018825
    0.012632  0.022848
    0.01788   0.026585
    0.023901  0.030001
    0.030604  0.033068
    0.0379    0.035771
    0.045705  0.038107
    0.053933  0.040075
    0.06254   0.04169
    0.071451  0.042966
    0.08063   0.043916
    0.090039  0.044561
    0.09968   0.044922
    0.109361  0.044999
    0.12      0.044952
    0.135773  0.04495
    0.151899  0.04493
    0.16806   0.044913
    0.184232  0.044898
    0.200407  0.044882
    0.21658   0.044866
    0.232723  0.044847
    0.248578  0.044839
    0.262095  0.044564
    0.274184  0.043576
    0.285768  0.041795
    0.296701  0.039168
    0.306379  0.035928
]
Example block output
Note

The body geometry coordinates must be input as columns of z (axial) and r (radial) coordinates, in that order.

Rotor Geometry

The next step is to assemble an object of type Rotor which contains the geometric information required to define the rotor(s) and their respective blade elements. In this example, we have a single rotor defined as follows.

# number of rotors
B = 5

# rotor axial location
rotor_axial_position = 0.12

# rotor tip radius
Rtip = 0.15572081487373543

# rotor hub radius
Rhub = 0.04495252299071941

# non-dimensional blade element radial stations
r = [
    0.050491
    0.061567
    0.072644
    0.083721
    0.094798
    0.10587
    0.11695
    0.12803
    0.13911
    0.15018
] ./ Rtip

# dimensional chord lengths
chords = [
    0.089142
    0.079785
    0.0713
    0.063979
    0.057777
    0.052541
    0.048103
    0.044316
    0.041061
    0.038243
]

# twist angles (from plane of rotation) in radians
twists = [
    69.012
    59.142
    51.825
    46.272
    41.952
    38.509
    35.699
    33.354
    31.349
    29.596
] .* pi / 180.0

# DFDC-type airfoil object
afparams = DuctAPE.c4b.DFDCairfoil(;
    alpha0=0.0,
    clmax=1.5,
    clmin=-1.0,
    dclda=6.28,
    dclda_stall=0.5,
    dcl_stall=0.2,
    cdmin=0.012,
    clcdmin=0.1,
    dcddcl2=0.005,
    cmcon=0.0,
    Re_ref=2e5,
    Re_exp=0.35,
    mcrit=0.7,
)

# all airfoils are the same
# NOTE: airfoils are inputs as a vector of vectors rather than a matrix
airfoils = [fill(afparams, length(r))] # specify the airfoil array

# assemble rotor parameters
rotor = DuctAPE.Rotor(
    B,
    rotor_axial_position,
    r,
    Rhub,
    Rtip,
    chords,
    twists,
    [0.0], # tip gap: currently only zero tip gaps work.
    airfoils,
    [0.0], # is_stator: can flip the cl lookups on the fly if desired, say, for stator sections
)
Example block output

Paneling Constants

The PanelingConstants object contains the constants required for DuctAPE to re-panel the provided geometry into a format compatible with the solve structure. Specifically, the DuctAPE solver makes some assumptions on the relative positioning of the body surfaces relative to the wakes and each other; and this is most easily guarenteed by a re-paneling of the provided body surface geometry.

# number of panels for the duct inlet
num_duct_inlet_panels = 30

# number of panels for the center body inlet
num_center_body_inlet_panels = 30

# number of panels from:
#  - rotor to duct trailing edge
#  - duct trailing edge to center body trailing edge
#  - center body trailing edge to end of wake
num_panels = [30, 1, 30]

# the duct trailing edge is ahead of the center_body trailing edge.
dte_minus_cbte = -1.0

# number of wake sheets (one more than blade elements to use)
num_wake_sheets = 11

# non-dimensional wake length aft of rear-most trailing edge
wake_length = 0.8

# assemble paneling constants
paneling_constants = DuctAPE.PanelingConstants(
    num_duct_inlet_panels,
    num_center_body_inlet_panels,
    num_panels,
    dte_minus_cbte,
    num_wake_sheets,
    wake_length,
)

Assembling the DuctedRotor

We are now posed to construct the DuctedRotor input type.

# assemble ducted_rotor object
ducted_rotor = DuctAPE.DuctedRotor(
    duct_coordinates,
    center_body_coordinates,
    rotor,
    paneling_constants,
)

Operating Point

Next we will assemble the operating point which contains information about the freestream as well as the rotor rotation rate(s).

# Freestream
Vinf = 30.0
rhoinf = 1.226
asound = 340.0
muinf = 1.78e-5

# Rotation Rate
RPM = 8000.0
Omega = RPM * pi / 30 # if using RPM, be sure to convert to rad/s

# utilizing the constructor function to put things in vector types
operating_point = DuctAPE.OperatingPoint(Vinf, Omega, rhoinf, muinf, asound)

Reference Parameters

The reference parameters are used in the post-processing non-dimensionalizations.

# reference velocity (close to average axial velocity at rotor in this case)
Vref = 50.0

# reference radius (usually tip radius of rotor)
Rref = Rtip

# assemble reference parameters
reference_parameters = DuctAPE.ReferenceParameters([Vref], [Rref])

Set Options

The default options should be sufficient for just starting out and are set through the set_options function.

options = DuctAPE.set_options()
Options{Bool, DuctAPE.HeadsBoundaryLayerOptions{Bool, Float64, Float64, typeof(RK2), Int64, Float64, Float64, Float64, DuctAPE.RK, Nothing, Nothing}, Vector{Bool}, Float64, Vector{Int64}, Vector{String}, Vector{String}, DuctAPE.var"#55#60", IntegrationOptions{GaussLegendre{Vector{Float64}, Vector{Float64}}, GaussLegendre{Vector{Float64}, Vector{Float64}}}, ModCSORSolverOptions{Bool, Float64, Int64, @NamedTuple{nrf::Float64, bt1::Float64, bt2::Float64, pf1::Float64, pf2::Float64, btw::Float64, pfw::Float64}}, GridSolverOptions{Bool, Float64, Int64, Symbol}}(false, true, [1], DuctAPE.var"#55#60"(), true, false, 0.05, 1.0e-15, 2.220446049250313e-15, IntegrationOptions{GaussLegendre{Vector{Float64}, Vector{Float64}}, GaussLegendre{Vector{Float64}, Vector{Float64}}}(GaussLegendre{Vector{Float64}, Vector{Float64}}([0.019855071751231856, 0.10166676129318664, 0.2372337950418355, 0.4082826787521751, 0.591717321247825, 0.7627662049581645, 0.8983332387068134, 0.9801449282487682], [0.05061426814518838, 0.11119051722668723, 0.15685332293894372, 0.18134189168918097, 0.18134189168918097, 0.15685332293894372, 0.11119051722668723, 0.05061426814518838]), GaussLegendre{Vector{Float64}, Vector{Float64}}([0.019855071751231856, 0.10166676129318664, 0.2372337950418355, 0.4082826787521751, 0.591717321247825, 0.7627662049581645, 0.8983332387068134, 0.9801449282487682], [0.05061426814518838, 0.11119051722668723, 0.15685332293894372, 0.18134189168918097, 0.18134189168918097, 0.15685332293894372, 0.11119051722668723, 0.05061426814518838])), DuctAPE.HeadsBoundaryLayerOptions{Bool, Float64, Float64, typeof(RK2), Int64, Float64, Float64, Float64, DuctAPE.RK, Nothing, Nothing}(false, true, true, false, 5000, 1.0e-6, nothing, nothing, 0.001, DuctAPE.RK(), DuctAPE.RK2, 3.0, 10, 10, 0.2, 0.2, false, 0.0, 0.0001, 0.0, false), Bool[0], ["outputs.jl"], false, ["outs"], GridSolverOptions{Bool, Float64, Int64, Symbol}(30, 3.0e-10, :newton, :forward, false, 3, Bool[0], [0], [0.0]), ModCSORSolverOptions{Bool, Float64, Int64, @NamedTuple{nrf::Float64, bt1::Float64, bt2::Float64, pf1::Float64, pf2::Float64, btw::Float64, pfw::Float64}}(false, 500, (nrf = 0.4, bt1 = 0.2, bt2 = 0.6, pf1 = 0.4, pf2 = 0.5, btw = 0.6, pfw = 1.2), 5.0e-10, Bool[0], [0], [-1.0]), true)

For more advanced option selection, see the examples and API reference.

Run a Single Analysis

With the ducted_rotor input build, the operation point set, and the options selected, we are now ready to run an analysis. This is done simply with the analyze function which dispatches the appropriate analysis, solve, and post-processing functions based on the selected options.

outs, success_flag = DuctAPE.analyze(
    ducted_rotor, operating_point, reference_parameters, options
)

Single Run Outputs

There are many outputs contained in the named tuple output from the analyze function, but some that may be of immediate interest include:

# Total Thrust Coefficient
outs.totals.CT
0.4995120496885003
# Total Torque Coefficient
outs.totals.CQ
0.09522170321731735

Run a Multi-Point Analysis

In the case that one wants to run the same geometry at several different operating points, for example: for a range of advance ratios, there is another dispatch of the analyze function that accepts an input, multipoint, that is a vector of operating points. Running a multi-point analysis on the example geometry given there, it might look something like this:

# - Advance Ratio Range - #
Js = range(0.0, 2.01; step=0.01)

# - Calculate Vinfs - #
D = 2.0 * rotor.Rtip[1] # rotor diameter
n = RPM / 60.0 # rotation rate in revolutions per second
Vinfs = Js * n * D

# - Set Operating Points - #
operating_points = [deepcopy(operating_point) for i in 1:length(Vinfs)]
for (iv, v) in enumerate(Vinfs)
    operating_points[iv].Vinf[] = v
end

# - Run Multi-point Analysis - #
outs_vec, success_flags = DuctAPE.analyze(
    ducted_rotor,
    operating_points,
    reference_parameters,
    DuctAPE.set_options(operating_points),
)

There are a few things to note here.

  1. We want to make sure that the operating point objects we put into the input vector are unique instances.
  2. We need to use the dispatch of set_options that accepts the operating point vector to set up the right number of things in the background (like convergence flags for each operating point).
  3. The outputs of the analysis are vectors of the same outputs for a single analysis.

Multi-point Outputs

For multi-point analysis outputs, which are given as a vector of output objects, we might access and plot things as follows. We also take the opportunity to present some verification against DFDC, showing that DuctAPE matches remarkably well (within 0.5%) of DFDC. We therefore first provide data from DFDC analyses of the above example geometry at various advance ratios.

# Verification Data From DFDC

dfdc_jept = [
    0.0 0.0 0.64763 0.96692
    0.1 0.1366 0.64716 0.88394
    0.2 0.2506 0.6448 0.80785
    0.3 0.3457 0.64044 0.73801
    0.4 0.4251 0.63401 0.67382
    0.5 0.4915 0.62534 0.61468
    0.6 0.547 0.61428 0.56001
    0.7 0.5935 0.6006 0.50925
    0.8 0.6326 0.58411 0.46187
    0.9 0.6654 0.56452 0.41738
    1.0 0.693 0.54158 0.37531
    1.1 0.716 0.51499 0.33522
    1.2 0.7349 0.48446 0.2967
    1.3 0.7499 0.44966 0.25937
    1.4 0.7606 0.41031 0.2229
    1.5 0.7661 0.36604 0.18694
    1.6 0.7643 0.31654 0.15121
    1.7 0.7506 0.26153 0.11547
    1.8 0.7126 0.20061 0.07941
    1.9 0.61 0.13355 0.04287
    2.0 0.1861 0.05993 0.00558
]

dfdc_J = dfdc_jept[:,1]
dfdc_eta = dfdc_jept[:,2]
dfdc_cp = dfdc_jept[:,3]
dfdc_ct = dfdc_jept[:,4]

We can then access the various multi-point analysis outputs however is convenient, we choose a broadcasting approach here:

# - extract efficiency, power, and thrust coefficients - #

# efficiency
eta = (p->p.totals.total_efficiency[1]).(outs_vec)

# power
c_p = (p->p.totals.CP[1]).(outs_vec)

# thrust
c_t = (p->p.totals.CT[1]).(outs_vec)

And then we can plot the data to compare DFDC and DuctAPE.

using Plots
using LaTeXStrings

# set up efficiency plot
pe = plot(; xlabel="Advance Ratio", ylabel=L"\eta")

# plot DFDC data
scatter!(
    pe,
    dfdc_J,
    dfdc_eta;
    markersize=5,
    label="DFDC",
)

# Plot DuctAPE outputs
plot!(
    pe,
    Js,
    eta;
    linewidth=2,
    label="DuctAPE",
)

# setup c_p/c_t plot
ppt = plot(; xlabel="Advance Ratio")

# plot DFDC data
scatter!(
    ppt,
    dfdc_J,
    dfdc_cp;
    markersize=5,
    label=L"\mathrm{DFDC}~~c_P",
)

scatter!(
    ppt,
    dfdc_J,
    dfdc_ct;
    markersize=5,
    label=L"\mathrm{DFDC}~~c_T",
)

# plot DuctAPE outputs
plot!(
    ppt,
    Js,
    c_p;
    linewidth=1.5,
    label=L"\mathrm{DuctAPE}~~c_P",
)

plot!(
    ppt,
    Js,
    c_t;
    linewidth=1.5,
    label=L"\mathrm{DuctAPE}~~c_T",
)

plot(
    pe,
    ppt;
    size=(700, 350),
    layout=(1, 2),
)
Example block output