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
60 changes: 31 additions & 29 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
nothing over the first and last `1/2N` of chord; the old code extrapolated
`Cp` off the end of each surface independently. Through a stagnation point
`ue` is linear in arc length while `Cp` is quadratic, so that extrapolated a
parabola through its own turning point: on an SK100 section it put
parabola through its own turning point: on an inflated section it put
`Cp = -1.60` at the leading edge, where it has to approach `+1`, and the two
surfaces disagreed at the trailing edge in violation of the Kutta condition.
Signing the lower surface negative makes both surfaces one continuous curve
Expand All @@ -22,29 +22,31 @@

### Added

- BREAKING: the `TAYLOR` aero model is gone, with `taylor_value` and
`set_taylor_polar!`. An order-2 polynomial cannot hold a stall knee: fitted
over a window wider than the knee it averages across it, narrower and it never
reaches it, and past the window it continued on whatever edge slope it ended
with — which on an SK100 tip panel was negative where the local slope was
`+0.05`/deg, and took the circulation solve to `NaN`. `SAMPLED` replaces it.
- `SAMPLED` aero model: cl/cd/cm sampled at ascending angles of attack per
panel, interpolated between them and held flat past either end. This is what a
live polar source writes every solve; `set_sampled_polar!` rewrites a panel's
knots and values in place. It spans only the angles it was sampled over, so it
ignores `delta` and is skipped by the stall-angle scan — but inside that range
it represents a stall, which is what a local fit cannot do.
- 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
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.
- 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
deflection given at arbitrary chord fractions — beam nodes, membrane nodes —
onto that basis. `LivePolars` and `refresh_live_polars!` then evaluate
NeuralFoil on a grid of angles per panel in one batched forward pass and write
those values in as each panel's `SAMPLED` polar, so a chordwise deformation
reaches the aerodynamics as a shape change rather than a flap angle. The grid
moves with the panel — `LivePolarSettings` carries the offsets off its current
angle of attack — and `polar_drift` reports how far the solve has left it,
past which the polar holds its last sampled value instead of extrapolating.
onto that basis. `chord_residual` first takes the straight line through the
deflection's own endpoints off it, which the basis cannot express and answers
with runaway weights; `chord_line` returns that line, since its rotation is
one the caller may still owe its angle of attack. `panel_kulfan_parameters`
fits each panel's contour once at build time, shrink-wrapped first because a
raw slice fits to weights that oscillate far more than any deformation will
and leave neighbouring panels disagreeing enough to cost the solve its
convergence. `LivePolars` and `refresh_live_polars!` then evaluate NeuralFoil
on a grid of angles per panel in one batched forward pass and write those
values in as each panel's polar table, so a chordwise deformation reaches the
aerodynamics as a shape change rather than a flap angle. The grid moves with
the panel — `LivePolarSettings` carries the offsets off its current angle of
attack — and `polar_drift` reports how far the solve has left it, past which
the polar holds its last sampled value instead of extrapolating.
- Live surface pressure in `AirfoilAero`: `refresh_live_pressure!` regenerates
every panel's `Cp` from its deformed shape in one batched forward pass at the
converged angle of attack, resampled onto the panel's own contour nodes by
Expand All @@ -61,15 +63,15 @@
table generator reconstruct pressure the same way.
- `compare_live_polar` solves one panel's current deformed shape in XFoil and
puts the answer next to the live polar the panel is flying, at the same angle
and Reynolds and under `live_xfoil_solver`'s matched transition settings. XFoil
is marched out from zero rather than jumped to the angle — the viscous march
refuses a blunt inflated section otherwise, and on the SK100's deformed
mid-span it refused every one — and the comparison is made at the nearest angle
it reached, reported as `alpha` beside the `requested` one. A
`LivePolars` now keeps each panel's analysis confidence, so the panel worth
asking about is the one to hand. NeuralFoil's confidence scores the input
shape rather than the answer, so a deformed section far from its training set
reads low whether or not the polar is wrong; this is what tells the two apart.
and Reynolds and under `live_xfoil_solver`'s matched transition settings.
XFoil is marched out from zero rather than jumped to the angle — the viscous
march refuses a blunt inflated section otherwise, and on a deformed mid-span
panel it refused every one — and the comparison is made at the nearest angle
it reached, reported as `alpha` beside the `requested` one. A `LivePolars` now
keeps each panel's analysis confidence, so the panel worth asking about is the
one to hand. NeuralFoil's confidence scores the input shape rather than the
answer, so a deformed section far from its training set reads low whether or
not the polar is wrong; this is what tells the two apart.
- `prepare_inputs` accepts one Kulfan shape per case, so a whole wing goes
through NeuralFoil in a single forward pass.
- A panel carries the deformed airfoil its polar was generated from as
Expand Down
7 changes: 5 additions & 2 deletions docs/src/private_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,11 @@ update_non_deformed_sections!
### Aerodynamic data and Cp
```@docs
calculate_new_aero_data
set_sampled_polar!
sampled_value
set_polar!
rebuild_polar
same_knots
polar_knots
window_alpha
assemble_polar_matrix
load_matrix_polar_data
read_aero_matrix
Expand Down
2 changes: 2 additions & 0 deletions docs/src/private_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CurrentModule = VortexStepMethod
### Wing Geometry, Panel and Aerodynamics
```@docs
Panel
ScanKnots
KulfanParameters
PanelProperties
Filament
Expand All @@ -17,6 +18,7 @@ SemiInfiniteFilament
### Aerodynamic model constants
```@docs
LEI_AIRFOIL_BREUKELS
SCAN_KNOT_MAX
```

### Airfoil aerodynamics (AirfoilAero)
Expand Down
18 changes: 6 additions & 12 deletions src/VortexStepMethod.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export calculate_projected_area, calculate_span
export MVec3

export LLT, Model, VSM
export AeroModel, INVISCID, POLY, LEI_AIRFOIL_BREUKELS, POLAR_MATRICES, POLAR_VECTORS, SAMPLED
export AeroModel, INVISCID, POLY, LEI_AIRFOIL_BREUKELS, POLAR_MATRICES, POLAR_VECTORS
export KulfanParameters
export BILLOWING, COSINE, LINEAR, PanelDistribution, SPLIT_PROVIDED, UNCHANGED
export ELLIPTIC, InitialGammaDistribution, ZEROS
Expand Down Expand Up @@ -238,21 +238,19 @@ Enumeration of the implemented wing types.
@enum WingType RECTANGULAR CURVED ELLIPTICAL

"""
AeroModel `POLY` `POLAR_VECTORS` `POLAR_MATRICES` `INVISCID` `SAMPLED`
AeroModel `POLY` `POLAR_VECTORS` `POLAR_MATRICES` `INVISCID`

Enumeration of the implemented aerodynamic models. See also: [`AeroData`](@ref)

# Elements
- `POLY`: α-polynomial coefficients for cl/cd/cm (e.g. Breukels LEI coeffs, generated
by the `AirfoilAero` package). Core only evaluates the polynomial.
- `POLAR_VECTORS`: Polar vectors as function of alpha (lookup tables with interpolation)
- `POLAR_VECTORS`: Polar vectors as function of alpha (lookup tables with interpolation).
A panel's table may be rewritten at run time by [`set_polar!`](@ref
VortexStepMethod.set_polar!), which is how a live polar source regenerates it from the
panel's deformed shape each solve.
- `POLAR_MATRICES`: Polar matrices as function of alpha and delta (lookup tables with interpolation)
- INVISCID
- `SAMPLED`: cl/cd/cm sampled at ascending angles of attack per panel, interpolated
between them and held flat past either end. This is what a live polar source writes
each solve, see [`refresh_live_polars!`](@ref
VortexStepMethod.AirfoilAero.refresh_live_polars!). Samples are the polar, so a stall
knee inside the sampled range is represented rather than smoothed. Ignores `delta`.

`LEI_AIRFOIL_BREUKELS` is a deprecated alias of `POLY`.

Expand All @@ -263,7 +261,6 @@ where `alpha` is the angle of attack, `delta` is trailing edge angle.
POLAR_VECTORS
POLAR_MATRICES
INVISCID
SAMPLED
end

"""
Expand Down Expand Up @@ -346,9 +343,6 @@ Union of different definitions of the aerodynamic properties of a wing section.
- (`alpha_range`, `cl_vector`, `cd_vector`, `cm_vector`) for `POLAR_VECTORS`
- (`alpha_range`, `delta_range`, `cl_matrix`, `cd_matrix`, `cm_matrix`) for `POLAR_MATRICES`

`SAMPLED` carries no section-level data: it is written onto a panel at run time by a
live polar source, never read from a section.

where `alpha` is the angle of attack [rad], `delta` is trailing edge angle [rad], `cl` the lift coefficient,
`cd` the drag coefficient and `cm` the pitching moment coefficient. The camber of a kite refers to
the curvature of its airfoil shape. The camber is typically measured as the maximum distance
Expand Down
2 changes: 1 addition & 1 deletion src/airfoil_aero/AirfoilAero.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ using NPZ
using Xfoil
using Printf: @sprintf
using ..VortexStepMethod: SectionAero, interpolate_matrix_nans!, delta_suffix,
write_node_rows, section_surface, set_sampled_polar!,
write_node_rows, section_surface, set_polar!,
KulfanParameters, calculate_cl, calculate_cd,
calculate_cm

Expand Down
16 changes: 8 additions & 8 deletions src/airfoil_aero/live_polar.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

How a live polar is sampled. Every solve, each panel's deformed shape is evaluated at
its reference angle of attack plus each of `offsets`, and those values become the
panel's `SAMPLED` polar directly — there is no fit in between.
panel's polar table directly — there is no fit in between.

Sampling rather than fitting is what lets a panel hold a stall. A polynomial over the
same window averages the knee into a slope and past the peak returns a lift slope of
Expand Down Expand Up @@ -156,14 +156,15 @@ end
refresh_live_polars!(live, panels, alpha_ref, reynolds; deflection=nothing)
-> Float64

Regenerate every panel's polar from its current shape and write it in as a `SAMPLED`
polar (see [`set_sampled_polar!`](@ref VortexStepMethod.set_sampled_polar!)). Per panel:
Regenerate every panel's polar table from its current shape and write it in
(see [`set_polar!`](@ref VortexStepMethod.set_polar!)). Per panel:
deform the base airfoil by `deflection` (a chord-normalized deflection on `live.basis.x`,
or `nothing` to keep the base shape), evaluate NeuralFoil at `alpha_ref .+ offsets`, and
hand those values straight to the panel.

The write is in place — same knot count, same vectors — so a refresh every solve costs
the forward pass and nothing else.
the forward pass and the three interpolation rebuilds `Interpolations` needs to take the
new values, which it copies rather than references.

Each panel keeps the deformed shape it was evaluated at as its `live_shape`, so a plot
draws the airfoil the network actually saw.
Expand Down Expand Up @@ -204,9 +205,8 @@ function refresh_live_polars!(live::LivePolars, panels, alpha_ref, reynolds;
samples = ((i - 1) * n_samples + 1):(i * n_samples)
live.knots .= alpha_vec[i] .+ offsets
live.confidence[i] = minimum(view(confidence, samples))
set_sampled_polar!(panels[i], live.knots, view(cl, samples),
view(cd, samples), view(cm, samples);
shape=live.deformed[i])
set_polar!(panels[i], live.knots, view(cl, samples), view(cd, samples),
view(cm, samples); shape=live.deformed[i])
end
return minimum(confidence)
end
Expand Down Expand Up @@ -239,7 +239,7 @@ pair stays like for like even when the march stops short.
Run it when `confidence` is low. A confidence is the network's opinion of its own
inputs — a shape far from what it was trained on scores badly whether or not the answer
is wrong — so it says to go and check, not what the check will find. `cl` here is read
off the panel's `SAMPLED` polar rather than from a fresh network call, so what is
off the panel's polar table rather than from a fresh network call, so what is
compared is what the solver actually flew.

A reference that will not converge anywhere on the ramp comes back `NaN` rather than
Expand Down
7 changes: 4 additions & 3 deletions src/body_aerodynamics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,10 @@ function calculate_stall_angle_list!(stall_angles::AbstractVector,
# Default stall angle if none found
panel_stall = stall_angle_if_none_detected

# A live polar only spans the window it was sampled over, so neither its
# curvature nor its flat ends may be read as a stall peak outside that.
if panel.aero_model == SAMPLED
# A table that stops short of the scan says nothing about a stall in it:
# past its end it is held flat, which is not a peak.
if panel.alpha_window > 0 &&
panel.alpha_ref + panel.alpha_window < deg2rad(begin_aoa)
stall_angles[idx] = panel_stall
continue
end
Expand Down
Loading
Loading