Skip to content
Merged
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
23 changes: 17 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@

### Added

- A `POLAR_VECTORS` panel's table can be rewritten at run time by `set_polar!`,
which is what a live polar source does every solve. The knots and values reuse
the panel's own storage, and the panel records the range the table covers, so
a table generated over a window around one angle of attack is held at its end
- A panel's polar table can be rewritten at run time by `set_polar!`, which is
what a live polar source does every solve. The knots and values reuse the
panel's own storage, and the panel records the range the table covers, so a
table generated over a window around one angle of attack is held at its end
values rather than extrapolated past them, and the stall-angle scan skips a
table that stops short of it. A rewritten table is an ordinary polar in every
other way: same evaluation, same `delta` handling, no separate aero model.
table that stops short of it. The table keeps the shape the panel was built
with, so a `POLAR_MATRICES` wing takes one too, at every `delta` it spans: a
regenerated polar carries its deflection as shape and says the same at each. A
rewritten table is an ordinary polar in every other way, with no separate aero
model.
- Live in-memory polars in `AirfoilAero`: `KulfanBasis` and `deform_kulfan`
deform a fixed Kulfan fit analytically (a matvec against a constant CST basis,
never a refit, which is non-unique); `control_point_deflection` resamples a
Expand Down Expand Up @@ -81,6 +84,14 @@
`KulfanParameters` moved from `AirfoilAero` into the core module for it; it is
still exported from both.

### Changed

- A `POLAR_VECTORS` table of at most 24 angles now scans for its knot rather
than bisecting, which is faster at that length and is what a live polar's grid
is; longer tables keep the binary search. The knot container is part of a
panel's interpolation types, so which one a wing uses is fixed when it is
built.

## VortexStepMethod v4.2.0 2026-08-25

### Added
Expand Down
27 changes: 23 additions & 4 deletions src/panel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,11 @@ set_polar!(panel::Panel{<:Any, Nothing}, alphas, cl, cd, cm; shape=nothing) =
"""
set_polar!(panel, alphas, cl, cd, cm; shape=nothing)

Rewrite a panel's `POLAR_VECTORS` table with values at `alphas` [rad], ascending, and
rebuild its interpolations. The panel's `alpha_ref` and `alpha_window` are taken from the
Rewrite a panel's polar table with values at `alphas` [rad], ascending, and rebuild its
interpolations. The table keeps the shape the panel was built with: a `POLAR_VECTORS`
panel takes the angles as they are, and a `POLAR_MATRICES` panel takes them at every
`delta` it already spans, since a regenerated polar carries its deflection as shape
rather than as a flap angle and so says the same thing at each. The panel's `alpha_ref` and `alpha_window` are taken from the
angles, so a table covering only a window around one angle is held at its ends rather than
extrapolated past them (see [`window_alpha`](@ref)).

Expand All @@ -317,7 +320,7 @@ function set_polar!(panel::Panel, alphas, cl, cd, cm; shape=nothing)
throw(ArgumentError("A polar table needs at least two angles."))
issorted(alphas) ||
throw(ArgumentError("A polar table needs ascending angles."))
panel.aero_model = POLAR_VECTORS
panel.aero_model = polar_model(panel.cl_interp)
panel.alpha_ref = (alphas[1] + alphas[end]) / 2
panel.alpha_window = (alphas[end] - alphas[1]) / 2
panel.live_shape = shape
Expand All @@ -344,14 +347,30 @@ container, so the rebuilt object has the type the panel's field was parameterise
Only the values need rebuilding — `Interpolations` holds the knots by reference — but
they are what it copies, so the object is rebuilt until it can take them in place.
"""
rebuild_polar(old, knots, values) =
rebuild_polar(old::Interpolations.Extrapolation{<:Any, 1}, knots, values) =
linear_interpolation(same_knots(old.itp.knots[1], knots), values;
extrapolation_bc=old.et)

function rebuild_polar(old::Interpolations.Extrapolation{<:Any, 2}, knots, values)
deltas = old.itp.knots[2]
return linear_interpolation((same_knots(old.itp.knots[1], knots), deltas),
repeat(values, 1, length(deltas));
extrapolation_bc=old.et)
end

"The angles in the container the panel's interpolations were parameterised with."
same_knots(::ScanKnots, knots) = ScanKnots(knots)
same_knots(::AbstractVector, knots) = knots

"""
polar_model(interp) -> AeroModel

The model a rewritten table leaves the panel on: the one its interpolations were built
with, since their shape is fixed by the panel's type.
"""
polar_model(::Interpolations.Extrapolation{<:Any, 1}) = POLAR_VECTORS
polar_model(::Interpolations.Extrapolation{<:Any, 2}) = POLAR_MATRICES

"""
reinit!(panel, section_1, section_2, aero_center, control_point, bound_point_1,
bound_point_2, x_airf, y_airf, z_airf, delta, vec; kwargs...)
Expand Down
27 changes: 27 additions & 0 deletions test/airfoil_aero/test_live_polar.jl
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ end
zeros(5))
end

@testset "a rewritten polar lands on a matrix panel too" begin
# A wing whose offline polars carry a delta axis still has to take a live polar:
# the deflection is in the shape it was generated from, so the table says the same
# at every delta rather than having no answer for one.
alpha = collect(deg2rad.(-15.0:1.0:15.0))
delta = collect(deg2rad.(-40.0:10.0:40.0))
na, nd = length(alpha), length(delta)
grid = [0.1 * i for i in 1:na, _ in 1:nd]
data = (alpha, delta, grid, fill(0.02, na, nd), fill(-0.05, na, nd))
section = Section([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], POLAR_MATRICES, data)
CL, CD, CM, CP = panel_interp_types(section, true)
panel = Panel{Float64, CL, CD, CM, CP}()
init_aero!(panel, section, section)

knots = collect(deg2rad.(-12.0:3.0:12.0))
cl = collect(range(0.0, 1.2, length(knots)))
set_polar!(panel, knots, cl, fill(0.03, 9), fill(-0.1, 9))

@test panel.aero_model == POLAR_MATRICES
@test calculate_cl(panel, knots[3]) ≈ cl[3]
# The flap axis is gone from the answer: every delta reads the same polar.
@test calculate_cl(panel, knots[3], deg2rad(-40.0)) ≈
calculate_cl(panel, knots[3], deg2rad(40.0)) ≈ cl[3]
# And it is held past the window it was sampled over, as on a vector panel.
@test calculate_cl(panel, deg2rad(40.0)) ≈ cl[end]
end

@testset "live polars" begin
base = KulfanParameters(fill(0.15, 8), fill(-0.05, 8), 0.0, 0.0)
n_panels = 3
Expand Down
Loading