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
20 changes: 9 additions & 11 deletions docs/src/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@ calculate_span
calculate_projected_area
```

## Surface-pressure (Cp) tables
The Cp table types and IO live in the core package; the generation of Cp tables
(from XFoil/NeuralFoil) lives in `AirfoilAero`.
## Surface aero (contour + Cp + cf) tables
The per-section surface aero table type and its loader live in the core package; the
generation and CSV/dat writing (from XFoil/NeuralFoil) live in `AirfoilAero`.
```@docs
CpData
CpPolar
read_cp_data
write_cp_data
cp_distribution
delta_cp
SectionAero
section_surface
read_section_aero
```

## Airfoil aerodynamics (AirfoilAero)
Expand Down Expand Up @@ -47,7 +44,9 @@ neuralfoil_aero
generate_aero_matrices
generate_polar_from_coordinates
generate_polar_from_dat
generate_cp_polar
generate_airfoil_aero
generate_section_aero
write_section_aero
```

## OBJ mesh conversion (ObjAdapter)
Expand All @@ -59,7 +58,6 @@ CurrentModule = VortexStepMethod.ObjAdapter
```
```@docs
obj_to_yaml
generate_section_polars
write_yaml
perpendicular_sections
plot_airfoils
Expand Down
16 changes: 9 additions & 7 deletions docs/src/private_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ calculate_new_aero_data
assemble_polar_matrix
load_matrix_polar_data
read_aero_matrix
write_aero_matrix
read_dat
read_node_table
interpolate_matrix_nans!
remove_vector_nans
generate_polar_data
extract_literature_polar_data
parse_literature_column
interpolate_cp_to_refined!
prepare_cp_output!
validate_cp_sections
interpolate_section_aero_to_refined!
validate_section_aero
```

### Examples
Expand Down Expand Up @@ -115,9 +115,7 @@ flood_outside
trace_level_set
largest_linking_gap
resample_arc
resample_x
smooth_turning!
split_surfaces
```

### NeuralFoil network
Expand All @@ -143,7 +141,11 @@ read_dat_coordinates
write_dat
write_polar_csv
write_polar_matrix_csv
fill_slice_nans!
write_aero_matrix
write_node_table
flat_plate_cf
neuralfoil_contour_solution
fill_node_nans!
```

## OBJ mesh conversion (ObjAdapter)
Expand Down
41 changes: 12 additions & 29 deletions examples/V3_neuralfoil.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ fig_slices_3d = plot_slices_3d(OBJ_PATH; n_slices=N_SLICES, rotation=ROTATION,
n_bins=N_BINS, wrap_method=WRAP)
GLMakie.save("V3_slices_3d.png", fig_slices_3d)

# Generate per-slice NeuralFoil polars from the shrink-wrapped sections. XFoil is also
# supported (pass `solver=XFoilSolver(...)`) but its panel method does not converge on
# the V3's thin, tube-nosed membrane airfoils, so NeuralFoil is used here.
println("Generating NeuralFoil polars from OBJ mesh...")
generate_section_polars(OBJ_PATH, POLARS_DIR; n_slices=N_SLICES, Re=RE,
rotation=ROTATION, n_bins=N_BINS, wrap_method=WRAP,
solver=NF_SOLVER, delta_range=DELTA_RANGE, verbose=true)
# `obj_to_yaml` generates the whole aero geometry from the OBJ: shape (.dat), polar CSV,
# and Cp/cf tables plus a geometry.yaml. XFoil (`aero_solver=XFoilSolver(...)`) also works
# but doesn't converge on the V3's thin membrane airfoils, so NeuralFoil is used here.
println("Generating V3 aero geometry from OBJ (obj_to_yaml)...")
gen_dir = joinpath("data", "TUDELFT_V3_KITE", "generated_neuralfoil")
nf_yaml = obj_to_yaml(OBJ_PATH, gen_dir; n_sections=N_SLICES, Re=RE,
rotation=ROTATION, wrap_method=WRAP, aero_solver=NF_SOLVER, verbose=true)

# Flight conditions
v_a = 10.0
Expand All @@ -82,29 +82,12 @@ body_cfd = BodyAerodynamics([wing_cfd])
VortexStepMethod.reinit!(body_cfd)
solver_cfd = Solver(body_cfd, settings_cfd)

# Build a wing whose every airfoil points at a generated POLAR_VECTORS CSV under
# `polars_subdir`. Repoints a copy of the awesIO geometry and loads it as a normal
# uniform POLAR_VECTORS wing — no manual section surgery needed.
using YAML
yaml_path = joinpath("data", "TUDELFT_V3_KITE", "aero_geometry.yaml")
function build_polar_wing(polars_subdir, out_name)
geom = YAML.load_file(yaml_path)
for (i, airfoil) in enumerate(geom["wing_airfoils"]["data"])
airfoil[2] = "polars"
airfoil[3] = Dict("csv_file_path" => "$polars_subdir/$i.csv")
end
out_yaml = joinpath("data", "TUDELFT_V3_KITE", out_name)
write_yaml(out_yaml, geom)
wing = Wing(out_yaml; n_panels=50, spanwise_distribution=LINEAR)
refine!(wing)
body = BodyAerodynamics([wing])
VortexStepMethod.reinit!(body)
s = Solver(body, settings_cfd)
return s, body
end

println("Creating wing with NeuralFoil polars...")
solver_nf, body_nf = build_polar_wing("polars_neuralfoil", "aero_geometry_neuralfoil.yaml")
wing_nf = Wing(nf_yaml; n_panels=50, spanwise_distribution=LINEAR)
refine!(wing_nf)
body_nf = BodyAerodynamics([wing_nf])
VortexStepMethod.reinit!(body_nf)
solver_nf = Solver(body_nf, settings_cfd)

# Compare CFD-polar and NeuralFoil-polar wings against published references
# (Poland 2025 RANS CFD and wind tunnel). `plot_polars` sweeps each solver over the
Expand Down
9 changes: 4 additions & 5 deletions src/VortexStepMethod.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export FAILURE, FEASIBLE, INFEASIBLE, SolverStatus
export LOOP, NONLIN, SolverType
export load_polar_data

# Surface-pressure (Cp) table types + IO (generation lives in AirfoilAero)
export CpData, CpPolar, cp_distribution, delta_cp
export read_cp_data, write_cp_data
# Per-section surface aero (contour + Cp + cf). Human-readable CSV/dat IO: reading here,
# writing in AirfoilAero.
export SectionAero, section_surface, read_section_aero

export plot_combined_analysis, plot_distribution, plot_geometry, plot_polar_data,
plot_polars, plot_section_polars, save_plot, show_plot
Expand Down Expand Up @@ -414,7 +414,7 @@ end

# Include core functionality
include("settings.jl")
include("cp_types.jl")
include("section_aero.jl")
include("wing_geometry.jl")
include("polars.jl")
include("yaml_geometry.jl")
Expand All @@ -423,7 +423,6 @@ include("panel.jl")
include("body_aerodynamics.jl")
include("wake.jl")
include("solver.jl")
include("cp_polars.jl")

include("plotting_helpers.jl")

Expand Down
9 changes: 5 additions & 4 deletions src/airfoil_aero/AirfoilAero.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ using Interpolations
using NPZ
using Xfoil
using Printf: @sprintf
using ..VortexStepMethod: CpData, CpPolar, interpolate_matrix_nans!, write_aero_matrix
using ..VortexStepMethod: SectionAero, interpolate_matrix_nans!

include("kulfan.jl")
include("shrink_wrap.jl")
Expand All @@ -19,7 +19,7 @@ include("airfoil_solvers/neuralfoil_solver.jl")
include("airfoil_io.jl")
include("polar_gen.jl")
include("polar_export.jl")
include("cp_gen.jl")
include("section_aero_gen.jl")

export KulfanParameters, KulfanFitMethod, LeastSquaresFit
export ShrinkWrap, shrink_wrap
Expand All @@ -28,9 +28,10 @@ export NeuralFoilModel, NeuralFoilResult, load_neuralfoil_model
export neuralfoil_aero, neuralfoil_section
export AbstractAirfoilSolver, XFoilSolver, NeuralFoilSolver
export SectionSolution, DeformedSection, deform_section, analyze_section, analyze_sweep
export create_2d_polars, generate_aero_matrices, generate_cp_polar, lei_poly_coeffs
export create_2d_polars, generate_aero_matrices, generate_section_aero, lei_poly_coeffs
export generate_airfoil_aero, write_section_aero
export turn_trailing_edge!, get_lower_upper
export read_dat_coordinates, write_dat, write_polar_csv
export read_dat_coordinates, write_dat, write_polar_csv, write_polar_matrix_csv
export generate_polar_from_coordinates, generate_polar_from_dat, resolve_airfoil

end
61 changes: 26 additions & 35 deletions src/airfoil_aero/airfoil_solvers/common.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@ abstract type AbstractAirfoilSolver end
"""
SectionSolution

Result of a single 2D analysis: integrated coefficients plus the surface pressure
as `(x/c, Cp)` per surface (each single-valued in x, leading edge to trailing
Result of a single 2D analysis: integrated coefficients plus the full closed surface
as node arrays `(x, y)` with surface pressure `cp` and skin friction `cf` per node
(one continuous contour, trailing edge → upper → leading edge → lower → trailing
edge). `confidence` is `1.0`/`NaN` for XFoil (converged/not) or NeuralFoil's
`analysis_confidence`. The generator resamples the `(x, Cp)` pairs onto the shared
chord slices.
`analysis_confidence`. Non-converged angles carry empty node arrays. `cf` is the
tangential skin-friction coefficient (exact from XFoil `bldump`, approximate from a
flat-plate closure for NeuralFoil).
"""
struct SectionSolution
alpha::Float64
cl::Float64
cd::Float64
cm::Float64
confidence::Float64
x_upper::Vector{Float64}
cp_upper::Vector{Float64}
x_lower::Vector{Float64}
cp_lower::Vector{Float64}
x::Vector{Float64}
y::Vector{Float64}
cp::Vector{Float64}
cf::Vector{Float64}
end

"""
Expand Down Expand Up @@ -113,33 +115,6 @@ function deform_section(x, y, delta; crease_frac=0.9, thickness_frac=1.0,
return DeformedSection(kulfan, xd, yd)
end

"""
split_surfaces(x, cp) -> (x_upper, cp_upper, x_lower, cp_lower)

Split a Selig-ordered `(x, cp)` node set at the leading edge (`argmin(x)`) into
upper and lower surfaces, each ordered leading edge → trailing edge (x increasing).
"""
function split_surfaces(x, cp)
le = argmin(x)
upper = le:-1:1
lower = le:length(x)
return x[upper], cp[upper], x[lower], cp[lower]
end

"""
resample_x(xs, values, targets) -> Vector{Float64}

Linearly interpolate `values` (given at `xs`) onto `targets`, sorting by `x` and
dropping zero-length segments so the breakpoints are strictly increasing.
"""
function resample_x(xs, values, targets)
p = sortperm(xs)
xs, values = xs[p], values[p]
keep = [true; diff(xs) .> 0]
itp = linear_interpolation(xs[keep], values[keep]; extrapolation_bc=Line())
return itp.(targets)
end

"""
analyze_sweep(solver, def, alpha_range, Re) -> Vector{SectionSolution}

Expand All @@ -150,3 +125,19 @@ NeuralFoil vectorization).
function analyze_sweep(solver::AbstractAirfoilSolver, def::DeformedSection, alpha_range, Re)
return [analyze_section(solver, def, a, Re) for a in alpha_range]
end

"""
flat_plate_cf(xc, Re) -> Float64

Approximate local skin-friction coefficient at chord fraction `xc` (0..1) for
Reynolds number `Re` (`Re_x = Re·xc`), taking the larger of the two standard
flat-plate correlations: the Blasius laminar solution `cf = 0.664·Re_x^(-1/2)` and
Prandtl's one-seventh-power-law turbulent estimate `cf = 0.027·Re_x^(-1/7)` (see
e.g. White, *Viscous Fluid Flow*; Schlichting & Gersten, *Boundary-Layer Theory*).
A stand-in per-node `cf` for backends that do not expose one (NeuralFoil); XFoil
returns the exact distribution via `bldump`.
"""
function flat_plate_cf(xc, Re)
rex = max(Re * max(xc, 1e-3), 1.0)
return max(0.664 / sqrt(rex), 0.027 / rex^(1 / 7))
end
27 changes: 23 additions & 4 deletions src/airfoil_aero/airfoil_solvers/neuralfoil_solver.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,34 @@ end
analyze_sweep(solver::NeuralFoilSolver, def, alpha_range, Re) -> Vector{SectionSolution}

Evaluate all angles (radians) in one vectorized NeuralFoil call on the deformed
Kulfan parameters. Cp comes at NeuralFoil's fixed station x/c.
Kulfan parameters, then map the station `Cp` onto the deformed contour nodes
(`def.x`, `def.y`) — upper nodes from `cp_upper`, lower from `cp_lower`. `cf` is a
flat-plate closure ([`flat_plate_cf`](@ref)), NeuralFoil not exposing skin friction.
"""
function analyze_sweep(solver::NeuralFoilSolver, def::DeformedSection, alpha_range, Re)
res = neuralfoil_section(def.kulfan, rad2deg.(collect(alpha_range)), Re;
model_size=solver.model_size, weights_dir=solver.weights_dir,
n_crit=solver.n_crit, xtr_upper=solver.xtr_upper, xtr_lower=solver.xtr_lower)
return [SectionSolution(alpha_range[i], res.cl[i], res.cd[i], res.cm[i],
res.confidence[i], res.x, res.cp_upper[:, i],
res.x, res.cp_lower[:, i]) for i in eachindex(alpha_range)]
x, y = collect(float.(def.x)), collect(float.(def.y))
le = argmin(x)
cf = [flat_plate_cf(clamp(xk, 0.0, 1.0), Re) for xk in x]
return [neuralfoil_contour_solution(alpha_range[i], res, i, x, y, le, cf)
for i in eachindex(alpha_range)]
end

"""
neuralfoil_contour_solution(alpha, res, i, x, y, le, cf) -> SectionSolution

Assemble a full-contour [`SectionSolution`](@ref) for case `i` of a NeuralFoil sweep
`res`: interpolate `res.cp_upper`/`res.cp_lower` (at `res.x`) onto the contour nodes
`(x, y)` split at the leading edge `le`, carrying the precomputed `cf`.
"""
function neuralfoil_contour_solution(alpha, res, i, x, y, le, cf)
up = linear_interpolation(res.x, res.cp_upper[:, i]; extrapolation_bc=Line())
lo = linear_interpolation(res.x, res.cp_lower[:, i]; extrapolation_bc=Line())
cp = [(k <= le ? up : lo)(clamp(x[k], 0.0, 1.0)) for k in eachindex(x)]
return SectionSolution(alpha, res.cl[i], res.cd[i], res.cm[i],
res.confidence[i], x, y, cp, cf)
end

"""
Expand Down
14 changes: 9 additions & 5 deletions src/airfoil_aero/airfoil_solvers/xfoil_solver.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ end

Set the deformed coordinates once (repaneling if `solver.repanel`), then solve
`alpha_range` (radians) sweeping negative and positive angles outward from zero with a
reinit at each side for convergence. Non-converged angles yield empty Cp and `NaN`
confidence.
reinit at each side for convergence. Each converged angle reads the surface pressure
(`Xfoil.cpdump`) and the boundary layer (`Xfoil.bldump`, giving `cf` and the node
coordinates) at the same panel nodes. Non-converged angles yield empty node arrays and
`NaN` confidence.
"""
function analyze_sweep(solver::XFoilSolver, def::DeformedSection, alpha_range, Re)
Xfoil.set_coordinates(def.x, def.y)
Expand All @@ -50,9 +52,11 @@ function analyze_sweep(solver::XFoilSolver, def::DeformedSection, alpha_range, R
xtrip=solver.xtrip, ncrit=solver.ncrit)
reinit = false
if converged
xc, cp = Xfoil.cpdump()
xu, cu, xl, cl2 = split_surfaces(xc, cp)
sols[ia] = SectionSolution(alpha_range[ia], cl, cd, cm, 1.0, xu, cu, xl, cl2)
_, cp = Xfoil.cpdump()
_, xb, yb, _, _, _, cf = Xfoil.bldump()
n = min(length(cp), length(cf))
sols[ia] = SectionSolution(alpha_range[ia], cl, cd, cm, 1.0,
xb[1:n], yb[1:n], cp[1:n], cf[1:n])
else
sols[ia] = SectionSolution(alpha_range[ia], NaN, NaN, NaN, NaN,
Float64[], Float64[], Float64[], Float64[])
Expand Down
Loading
Loading