Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
doi:10.1088/1742-6596/2767/2/022068): each section gets a drag increment and a force
along its span from the flow across it, in `solve!`, `solve` and `linearize`. Opt-in
via `is_with_viscous_drag_correction` (default `false`) on the solver settings.
- `linearize` takes a `BodyAerodynamics` with more than one wing; `theta_idxs` and
`delta_idxs` then run over the unrefined sections of all wings in order.

### Changed

Expand Down
1 change: 1 addition & 0 deletions docs/src/private_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ calculate_filaments_for_plotting
### Mesh refinement and billowing
```@docs
unrefined_deform!
unrefined_section_range
deform!
compute_refined_panel_mapping!
compute_refined_section_interpolation!
Expand Down
31 changes: 31 additions & 0 deletions src/body_aerodynamics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,37 @@ function calculate_stall_angle_list!(stall_angles::AbstractVector,
return nothing
end

"""
unrefined_section_range(body_aero::BodyAerodynamics, wing_idx)

Indices of the unrefined sections of wing `wing_idx` in a distribution that runs over the
unrefined sections of all wings in order, such as `moment_unrefined_dist`.
"""
function unrefined_section_range(body_aero::BodyAerodynamics, wing_idx)
offset = 0
for i in 1:wing_idx-1
offset += body_aero.wings[i].n_unrefined_sections
end
return offset .+ (1:body_aero.wings[wing_idx].n_unrefined_sections)
end

"""
unrefined_deform!(body_aero::BodyAerodynamics, theta_angles, delta_angles)

Deform each wing of `body_aero` by its entries of `theta_angles` and `delta_angles` [rad],
which run over the unrefined sections of all wings in order; `nothing` leaves that angle
unchanged. Call [`reinit!`](@ref) afterwards to update the panels.
"""
function unrefined_deform!(body_aero::BodyAerodynamics, theta_angles, delta_angles)
for (wing_idx, wing) in enumerate(body_aero.wings)
section_range = unrefined_section_range(body_aero, wing_idx)
unrefined_deform!(wing,
isnothing(theta_angles) ? nothing : view(theta_angles, section_range),
isnothing(delta_angles) ? nothing : view(delta_angles, section_range))
end
return nothing
end

"""
reinit!(body_aero::BodyAerodynamics; init_aero, va, omega, refine_mesh, recompute_mapping, sort_sections)

Expand Down
69 changes: 28 additions & 41 deletions src/solver.jl
Original file line number Diff line number Diff line change
Expand Up @@ -481,13 +481,13 @@ function calc_forces!(solver::Solver{P, U, T}, body_aero::BodyAerodynamics;
fill!(unrefined_count_dist, 0)

panel_idx = 1
unrefined_idx = 1
for wing in body_aero.wings
for (wing_idx, wing) in enumerate(body_aero.wings)
if wing.n_unrefined_sections > 0
section_range = unrefined_section_range(body_aero, wing_idx)
for local_panel_idx in 1:wing.n_panels
panel = body_aero.panels[panel_idx]
original_section_idx = wing.refined_panel_mapping[local_panel_idx]
target_unrefined_idx = unrefined_idx + original_section_idx - 1
target_unrefined_idx = section_range[original_section_idx]

# Accumulate coefficients and moments
moment_unrefined_dist[target_unrefined_idx] += moment_dist[panel_idx]
Expand All @@ -511,8 +511,7 @@ function calc_forces!(solver::Solver{P, U, T}, body_aero::BodyAerodynamics;

# Average coefficients and geometry. width and
# moment_coeff_unrefined_dist stay summed (extensive).
for i in 1:wing.n_unrefined_sections
target_unrefined_idx = unrefined_idx + i - 1
for target_unrefined_idx in section_range
if unrefined_count_dist[target_unrefined_idx] > 0
count = unrefined_count_dist[target_unrefined_idx]
moment_unrefined_dist[target_unrefined_idx] /= count
Expand All @@ -529,7 +528,6 @@ function calc_forces!(solver::Solver{P, U, T}, body_aero::BodyAerodynamics;
# sum of panel widths in the unrefined section
end
end
unrefined_idx += wing.n_unrefined_sections
else
# Skip panels for wings with no unrefined sections
panel_idx += wing.n_panels
Expand Down Expand Up @@ -1177,16 +1175,19 @@ function _wing_with_eltype(wing::Wing{P, Float64}, ::Type{TD}) where {P, TD}
wing.spanwise_distribution,
PanelProperties{P, TD}(),
MVector{3, TD}(wing.spanwise_direction),
Section{TD}[_section_with_eltype(s, TD) for s in wing.unrefined_sections],
Section{TD}[_section_with_eltype(s, TD) for s in wing.refined_sections],
Section{TD}[_section_with_eltype(section, TD)
for section in wing.unrefined_sections],
Section{TD}[_section_with_eltype(section, TD)
for section in wing.refined_sections],
wing.remove_nan,
wing.use_prior_polar,
wing.billowing_percentage,
TD(wing.crease_frac),
copy(wing.refined_panel_mapping),
copy(wing.refined_section_left_idx),
Vector{TD}(wing.refined_section_weight),
Section{TD}[_section_with_eltype(s, TD) for s in wing.non_deformed_sections],
Section{TD}[_section_with_eltype(section, TD)
for section in wing.non_deformed_sections],
Vector{TD}(wing.theta_dist),
Vector{TD}(wing.delta_dist),
TD(wing.mass),
Expand All @@ -1213,10 +1214,8 @@ buffers are freshly allocated as `TD`-typed.
function make_dual_shadow(solver::Solver{P, U, Float64},
body_aero::BodyAerodynamics{P, W, Float64},
::Type{TD}) where {P, U, W, TD}
length(body_aero.wings) == 1 || throw(ArgumentError(
"make_dual_shadow currently supports body_aero with one wing"))
wing_d = _wing_with_eltype(body_aero.wings[1], TD)
body_aero_d = BodyAerodynamics([wing_d];
wings_d = [_wing_with_eltype(wing, TD) for wing in body_aero.wings]
body_aero_d = BodyAerodynamics(wings_d;
va = MVector{3, TD}(body_aero._va),
omega = MVector{3, TD}(body_aero.omega),
)
Expand Down Expand Up @@ -1252,8 +1251,8 @@ end
backend=AutoForwardDiff(), kwargs...)

Jacobian of aerodynamic outputs w.r.t. control and kinematic inputs at `y`. Each `*_idxs`
selects which entries of `y` map to twist angles (one per unrefined section), trailing-edge
deflections (one per unrefined section), apparent wind `(vx, vy, vz)`, and angular rate
selects which entries of `y` map to twist angles and trailing-edge deflections (one per
unrefined section, over all wings in order), apparent wind `(vx, vy, vz)`, and angular rate
`(ωx, ωy, ωz)` respectively.

`backend` accepts any `DifferentiationInterface` backend; `AutoForwardDiff()` (the default)
Expand All @@ -1264,7 +1263,7 @@ Returns `(jac, results, converged)` where `results` is `(F, M, moment_unrefined_
or the corresponding coefficients when `aero_coeffs=true` — and `converged` is `false` (with a
warning) if any internal solve missed the solver's tolerances.
"""
function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T};
function linearize(solver::Solver{<:Any, U}, body_aero::BodyAerodynamics, y::Vector{T};
theta_idxs=1:4,
delta_idxs=nothing,
va_idxs=nothing,
Expand All @@ -1273,22 +1272,12 @@ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T};
backend = AutoForwardDiff(),
fd_absstep::Float64=1e-8,
fd_relstep::Float64=1e-8,
kwargs...) where T
kwargs...) where {U, T}

!(length(body_aero.wings) == 1) && throw(ArgumentError("Linearization only works for a body_aero with one wing"))
wing = body_aero.wings[1]

# Validate that theta_idxs and delta_idxs match the number of unrefined sections
if !isnothing(theta_idxs) && wing.n_unrefined_sections > 0
length(theta_idxs) != wing.n_unrefined_sections && throw(ArgumentError(
"Length of theta_idxs ($(length(theta_idxs))) must match number of unrefined sections ($(wing.n_unrefined_sections))"))
end
if !isnothing(delta_idxs) && wing.n_unrefined_sections > 0
length(delta_idxs) != wing.n_unrefined_sections && throw(ArgumentError(
"Length of delta_idxs ($(length(delta_idxs))) must match number of unrefined sections ($(wing.n_unrefined_sections))"))
end
if wing.n_unrefined_sections == 0 && (!isnothing(theta_idxs) || !isnothing(delta_idxs))
throw(ArgumentError("Cannot use theta_idxs or delta_idxs when wing has no unrefined sections"))
for (name, idxs) in (("theta_idxs", theta_idxs), ("delta_idxs", delta_idxs))
isnothing(idxs) || length(idxs) == U || throw(ArgumentError(
"Length of $name ($(length(idxs))) must match number of unrefined sections " *
"($U)"))
end

n_failed = Ref(0)
Expand All @@ -1299,27 +1288,25 @@ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T};
if TI === Float64
body_aero_c = body_aero
solver_c = solver
wing_c = wing
else
shadow = shadow_ref[]
if shadow === nothing || eltype(shadow[1]._va) !== TI
shadow_ref[] = make_dual_shadow(solver, body_aero, TI)
end
body_aero_c, solver_c = shadow_ref[]
wing_c = body_aero_c.wings[1]
end

@views theta_angles = isnothing(theta_idxs) ? nothing : y_in[theta_idxs]
@views delta_angles = isnothing(delta_idxs) ? nothing : y_in[delta_idxs]

if !isnothing(theta_angles) || !isnothing(delta_angles)
VortexStepMethod.unrefined_deform!(wing_c, theta_angles, delta_angles; smooth=false)
VortexStepMethod.reinit!(body_aero_c; init_aero=false)
unrefined_deform!(body_aero_c, theta_angles, delta_angles)
reinit!(body_aero_c; init_aero=false)
end

va_vec = isnothing(va_idxs) ? MVector{3, TI}(body_aero_c._va) : y_in[va_idxs]
om = isnothing(omega_idxs) ? MVector{3, TI}(body_aero_c.omega) : y_in[omega_idxs]
set_va!(body_aero_c, va_vec, om)
omega = isnothing(omega_idxs) ? MVector{3, TI}(body_aero_c.omega) : y_in[omega_idxs]
set_va!(body_aero_c, va_vec, omega)

solve!(solver_c, body_aero_c; kwargs...)
solver_c.lr.converged || (n_failed[] += 1)
Expand All @@ -1335,13 +1322,13 @@ function linearize(solver::Solver, body_aero::BodyAerodynamics, y::Vector{T};
return nothing
end

n_results = 3 + 3 + length(solver.sol.moment_unrefined_dist)
n_results = 3 + 3 + U
jac = zeros(n_results, length(y))
results = zeros(n_results)
be = backend === nothing ?
ad_backend = backend === nothing ?
AutoFiniteDiff(absstep=fd_absstep, relstep=fd_relstep) : backend
prep = prepare_jacobian(calc_results!, results, be, y)
jacobian!(calc_results!, results, jac, prep, be, y)
prep = prepare_jacobian(calc_results!, results, ad_backend, y)
jacobian!(calc_results!, results, jac, prep, ad_backend, y)
calc_results!(results, y)
converged = n_failed[] == 0
if !converged
Expand Down
79 changes: 73 additions & 6 deletions test/body_aerodynamics/test_body_aerodynamics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,32 @@ function solve_wings(wings)
return body_aero, solve!(Solver(body_aero), body_aero)
end

"""
wing_pair(section_y, n_panels, offset)

Two `inviscid_wing`s of `n_panels` panels, the second shifted by `-offset` [m] in y.
"""
function wing_pair(section_y, n_panels, offset)
return [inviscid_wing(section_y; n_panels),
inviscid_wing(section_y .- offset; n_panels)]
end

"""
linearize_body(body_aero; kwargs...)

`linearize` of `body_aero` at zero twist and deflection over the twist and the deflection of
every unrefined section, then the inflow and the angular rate.
"""
function linearize_body(body_aero; kwargs...)
n_sections = sum(wing -> wing.n_unrefined_sections, body_aero.wings)
solver = Solver(body_aero; use_gamma_prev=false, rtol=1e-10)
y0 = [zeros(2n_sections); body_aero.va; zeros(3)]
return VortexStepMethod.linearize(solver, body_aero, y0;
theta_idxs=1:n_sections, delta_idxs=n_sections+1:2n_sections,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR: delta_idxs is split per wing, but the INVISCID wings ignore deflection, so every delta column is zero and no assertion reads them. A swapped or shifted delta offset in unrefined_deform!(body_aero, ...) would pass both testsets.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 574b15b: the new testset "unrefined_deform! hands each wing its own run of angles" fails when wing 2 is handed wing 1's deflections (4884 passed, 1 failed).

va_idxs=2n_sections+1:2n_sections+3, omega_idxs=2n_sections+4:2n_sections+6,
kwargs...)
end

@testset "solve! on a two-wing body" begin
n_panels = 6
section_y = [2.0, 0.0, -2.0]
Expand All @@ -522,9 +548,7 @@ end
@test single.solver_status == FEASIBLE

@testset "wings far apart each act as the isolated wing" begin
wings = [inviscid_wing(section_y; n_panels),
inviscid_wing(section_y .- 1e4; n_panels)]
body_aero, sol = solve_wings(wings)
body_aero, sol = solve_wings(wing_pair(section_y, n_panels, 1e4))

@test length(body_aero.panels) == 2n_panels
@test sol.solver_status == FEASIBLE
Expand All @@ -536,14 +560,57 @@ end
end

@testset "wings one chord apart induce on each other" begin
wings = [inviscid_wing(section_y; n_panels),
inviscid_wing(section_y .- (span + 1.0); n_panels)]
_, sol = solve_wings(wings)
_, sol = solve_wings(wing_pair(section_y, n_panels, span + 1.0))
gamma = sol.gamma_distribution

@test sol.solver_status == FEASIBLE
@test gamma ≈ reverse(gamma) rtol=1e-6
@test gamma[n_panels] > 1.05single.gamma_distribution[n_panels]
@test sol.force[3] > 2single.force[3]
end

n_wing_sections = length(section_y)
first_sections = 1:n_wing_sections
second_sections = n_wing_sections+1:2n_wing_sections
@testset "unrefined_deform! hands each wing its own run of angles" begin
body_aero = BodyAerodynamics(wing_pair(section_y, n_panels, span + 1.0))
isolated = wing_pair(section_y, n_panels, span + 1.0)
theta = deg2rad.(1.0:2n_wing_sections)
delta = -2theta
VortexStepMethod.unrefined_deform!(body_aero, theta, delta)

for (wing_idx, section_range) in enumerate((first_sections, second_sections))
wing = isolated[wing_idx]
VortexStepMethod.unrefined_deform!(wing, theta[section_range],
delta[section_range])
@test VortexStepMethod.unrefined_section_range(body_aero, wing_idx) ==
section_range
@test body_aero.wings[wing_idx].theta_dist == wing.theta_dist
@test body_aero.wings[wing_idx].delta_dist == wing.delta_dist
end
end

@testset "linearize: theta of each wing moves that wing's sections" begin
body_aero, _ = solve_wings(wing_pair(section_y, n_panels, 1e4))
jac, _, converged = linearize_body(body_aero)
own_first = jac[6 .+ first_sections, first_sections]

@test converged
@test norm(own_first) > 0
@test jac[6 .+ second_sections, second_sections] ≈ own_first rtol=1e-9
@test norm(jac[6 .+ first_sections, second_sections]) < 1e-7norm(own_first)
@test norm(jac[6 .+ second_sections, first_sections]) < 1e-7norm(own_first)
end

@testset "linearize: AutoForwardDiff matches AutoFiniteDiff" begin
body_aero, _ = solve_wings(wing_pair(section_y, n_panels, span + 1.0))
jac_fwd, _, fwd_converged = linearize_body(body_aero)
jac_fd, _, fd_converged = linearize_body(body_aero; backend=nothing,
fd_absstep=1e-6, fd_relstep=1e-6)

@test fwd_converged
@test fd_converged
@test norm(jac_fwd[:, 1:2n_wing_sections]) > 0
@test jac_fwd ≈ jac_fd rtol=1e-4
end
end
Loading