diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a4d3a12..301c049b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/src/panel.jl b/src/panel.jl index d31d0535..3565ce07 100644 --- a/src/panel.jl +++ b/src/panel.jl @@ -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)). @@ -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 @@ -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...) diff --git a/test/airfoil_aero/test_live_polar.jl b/test/airfoil_aero/test_live_polar.jl index dafaedc3..58afab15 100644 --- a/test/airfoil_aero/test_live_polar.jl +++ b/test/airfoil_aero/test_live_polar.jl @@ -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