Skip to content
Open
32 changes: 21 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,10 @@ neuralfoil_aero
generate_aero_matrices
generate_polar_from_coordinates
generate_polar_from_dat
generate_cp_polar
generate_airfoil_aero
generate_section_aero
generate_airfoils
write_section_aero
```

## OBJ mesh conversion (ObjAdapter)
Expand All @@ -59,12 +59,22 @@ CurrentModule = VortexStepMethod.ObjAdapter
```
```@docs
obj_to_yaml
generate_section_polars
write_yaml
perpendicular_sections
plot_airfoils
plot_slices_3d
```

## Surfplan conversion (SurfplanAdapter)
These live in the `SurfplanAdapter` submodule of `VortexStepMethod`, which converts a
Surfplan export to the native YAML/CSV geometry format. Load it with
`using VortexStepMethod.SurfplanAdapter`.
```@meta
CurrentModule = VortexStepMethod.SurfplanAdapter
```
```@docs
surfplan_to_aero_yaml
```
```@meta
CurrentModule = VortexStepMethod
```
Expand Down
18 changes: 11 additions & 7 deletions docs/src/private_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ update_panel_properties!
build_interps
panel_interp_types
reinit!(wing::AbstractWing)
reinit!(panel::Panel, section_1::Section, section_2::Section, aero_center, control_point, bound_point_1, bound_point_2, x_airf, y_airf, z_airf, delta, vec, spanwise_direction)
rotated_te
calculate_filaments_for_plotting
```
Expand Down Expand Up @@ -74,15 +75,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 +116,7 @@ flood_outside
trace_level_set
largest_linking_gap
resample_arc
resample_x
smooth_turning!
split_surfaces
```

### NeuralFoil network
Expand All @@ -143,7 +142,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 Expand Up @@ -183,6 +186,7 @@ set_axes_equal_makie!
map_airfoil_3d
fitted_airfoil_3d
generated_slices
airfoil_skin_geometry
Makie.plot!(ax, panel::VortexStepMethod.Panel)
Makie.plot!(ax, body::VortexStepMethod.BodyAerodynamics)
Makie.plot!(body::VortexStepMethod.BodyAerodynamics)
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
141 changes: 115 additions & 26 deletions ext/VortexStepMethodMakieExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export plot_geometry, plot_distribution, plot_polars, save_plot, show_plot,

# Global storage for panel mesh observables (for dynamic plotting)
const PANEL_MESH_OBSERVABLES = Ref{Union{Nothing,Dict}}(nothing)
# Global storage for airfoil-skin observables, keyed by body objectid.
const AIRFOIL_SKIN_OBSERVABLES = Ref{Union{Nothing,Dict}}(nothing)

"""
plot!(ax, panel::VortexStepMethod.Panel; use_observables=false, kwargs...)
Expand Down Expand Up @@ -55,17 +57,105 @@ function Makie.plot!(ax, panel::VortexStepMethod.Panel; color=(:red, 0.2), R_b_w
end

"""
plot!(ax, body::VortexStepMethod.BodyAerodynamics; use_observables=false, kwargs...)
airfoil_skin_geometry(body; R_b_w=nothing, T_b_w=nothing) -> (vertices, faces, ribs)

Lofted airfoil skin of a `BodyAerodynamics`: each refined section's airfoil contour
(the `SectionAero` rest slice) placed on its chord and offset in thickness along the
airfoil upper-surface normal — the adjacent panel's `z_airf`, so the skin matches
VSM's own aero orientation (a `cross(chord, span)` fallback is used only if `z_airf`
is not yet populated). Transformed to world by `R_b_w`/`T_b_w`. `vertices`/`faces`
triangulate the skin between consecutive equal-node sections; `ribs` is one closed
contour polyline per section. Sections without contour data are skipped.
"""
function airfoil_skin_geometry(body; R_b_w=nothing, T_b_w=nothing)
to_world(p) = (isnothing(R_b_w) || isnothing(T_b_w)) ? Point3f(p) :
Point3f(R_b_w * p + T_b_w)
ribs = Vector{Point3f}[]
panel_offset = 0
for wing in body.wings
sections = wing.refined_sections
n = length(sections)
n_panels = n - 1
for (i, section) in enumerate(sections)
isnothing(section.section_aero) && continue
leading = Point3f(section.LE_point)
chord = Point3f(section.TE_point) - leading
chord_len = norm(chord)
chord_len < 1e-9 && continue
panel_idx = panel_offset + clamp(i, 1, max(n_panels, 1))
up = panel_idx <= length(body.panels) ?
Point3f(body.panels[panel_idx].z_airf) : Point3f(0, 0, 0)
if norm(up) < 1e-9 # not solved yet: geometric fallback
neighbor = Point3f(sections[i < n ? i + 1 : max(i - 1, 1)].LE_point)
spanwise = i < n ? neighbor - leading : leading - neighbor
up = cross(chord, spanwise)
end
up = norm(up) < 1e-9 ? Point3f(0, 0, 1) : Point3f(up / norm(up))
xs = section.section_aero.x
ys = section.section_aero.y
push!(ribs, [to_world(leading + xs[k, 1] * chord + (ys[k, 1] * chord_len) * up)
for k in axes(xs, 1)])
end
panel_offset += n_panels
end
vertices = Point3f[]
faces = Makie.GLTriangleFace[]
for i in 1:length(ribs) - 1
length(ribs[i]) == length(ribs[i + 1]) || continue
base = length(vertices)
append!(vertices, ribs[i])
append!(vertices, ribs[i + 1])
node_count = length(ribs[i])
for k in 1:node_count - 1
a1, a2 = base + k, base + k + 1
b1, b2 = base + node_count + k, base + node_count + k + 1
push!(faces, Makie.GLTriangleFace(a1, a2, b2))
push!(faces, Makie.GLTriangleFace(a1, b2, b1))
end
end
return vertices, faces, ribs
end

"""
plot!(ax, body::VortexStepMethod.BodyAerodynamics; use_observables=false,
airfoils=false, kwargs...)

Plot a `BodyAerodynamics` object by plotting each of its panels.
Plot a `BodyAerodynamics` object. By default draws each panel as a flat quad; with
`airfoils=true` instead draws the lofted airfoil skin (one see-through wing-shaped
mesh with a contour rib line per section, see [`airfoil_skin_geometry`](@ref)).

If `use_observables=true`, creates observables for dynamic updates keyed by (body_id, panel_index).
Otherwise, creates static plots (original behavior).
"""
function Makie.plot!(ax, body::VortexStepMethod.BodyAerodynamics; color=(:red, 0.2), R_b_w=nothing, T_b_w=nothing,
use_observables=false, kwargs...)
use_observables=false, airfoils=false,
airfoil_color=:deepskyblue, airfoil_opacity=0.2, rib_color=:black, kwargs...)
plots = []

if airfoils
vertices, faces, ribs = airfoil_skin_geometry(body; R_b_w, T_b_w)
skin_color = (airfoil_color, airfoil_opacity)
if use_observables
isnothing(AIRFOIL_SKIN_OBSERVABLES[]) && (AIRFOIL_SKIN_OBSERVABLES[] = Dict())
vertices_obs = Observable(vertices)
rib_obs = [Observable(rib) for rib in ribs]
isempty(faces) || push!(plots, mesh!(ax, vertices_obs, faces;
color=skin_color, transparency=true))
for rib in rib_obs
push!(plots, lines!(ax, rib; color=rib_color, transparency=true))
end
AIRFOIL_SKIN_OBSERVABLES[][objectid(body)] =
(vertices=vertices_obs, ribs=rib_obs)
else
isempty(faces) || push!(plots, mesh!(ax, vertices, faces;
color=skin_color, transparency=true))
for rib in ribs
push!(plots, lines!(ax, rib; color=rib_color, transparency=true))
end
end
return plots
end

if use_observables
# Initialize global storage if needed
if isnothing(PANEL_MESH_OBSERVABLES[])
Expand Down Expand Up @@ -122,33 +212,32 @@ Requires that `plot(body; use_observables=true)` or `plot!(ax, body; use_observa
was called first to create the observables.
"""
function Makie.plot!(body::VortexStepMethod.BodyAerodynamics; R_b_w=nothing, T_b_w=nothing, kwargs...)
# Check if observables exist
if isnothing(PANEL_MESH_OBSERVABLES[])
error("No panel observables found. Call plot(body; use_observables=true) first.")
end

body_id = objectid(body)

# Update each panel using stable (body_id, panel_idx) key
for (panel_idx, panel) in enumerate(body.panels)
key = (body_id, panel_idx)
if !haskey(PANEL_MESH_OBSERVABLES[], key)
error("No observables found for body $body_id panel $panel_idx. " *
"Call plot(body; use_observables=true) first.")
# Panel meshes (if plotted with use_observables): refresh from corner_points.
if !isnothing(PANEL_MESH_OBSERVABLES[])
for (panel_idx, panel) in enumerate(body.panels)
key = (body_id, panel_idx)
haskey(PANEL_MESH_OBSERVABLES[], key) || continue
obs = PANEL_MESH_OBSERVABLES[][key]
points = [Point3f(panel.corner_points[:, i]) for i in 1:4]
if !isnothing(R_b_w) && !isnothing(T_b_w)
points = [Point3f(R_b_w * p + T_b_w) for p in points]
end
obs.vertices[] = points
obs.border[] = [points..., points[1]]
end
end

# Get observables for this panel
obs = PANEL_MESH_OBSERVABLES[][key]

# Recompute vertices from current panel.corner_points
points = [Point3f(panel.corner_points[:, i]) for i in 1:4]
if !isnothing(R_b_w) && !isnothing(T_b_w)
points = [Point3f(R_b_w * p + T_b_w) for p in points]
# Airfoil skin (if plotted with use_observables): refresh from the current pose.
if !isnothing(AIRFOIL_SKIN_OBSERVABLES[]) &&
haskey(AIRFOIL_SKIN_OBSERVABLES[], body_id)
skin = AIRFOIL_SKIN_OBSERVABLES[][body_id]
vertices, _, ribs = airfoil_skin_geometry(body; R_b_w, T_b_w)
skin.vertices[] = vertices
for (i, rib) in enumerate(ribs)
i <= length(skin.ribs) && (skin.ribs[i][] = rib)
end

# Update observables
obs.vertices[] = points
obs.border[] = [points..., points[1]]
end

return nothing
Expand Down Expand Up @@ -1387,7 +1476,7 @@ given by its LE/TE points and leading-edge tangent (chord scale = `|TE - LE|`).
"""
function map_airfoil_3d(le, te, tangent, x, y)
frame = ObjAdapter.airfoil_frame(le, te, tangent)
frame === nothing && return nothing
(frame === nothing || isempty(x)) && return nothing
x_af, _, z_af = frame
chord = norm(te .- le)
return reduce(hcat, [le .+ x_af .* (x[i] * chord) .+ z_af .* (y[i] * chord)
Expand Down
10 changes: 5 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 All @@ -432,6 +431,7 @@ include("plotting_helpers.jl")
# `VortexStepMethod.AirfoilAero` / `.ObjAdapter`, or `using VortexStepMethod.AirfoilAero`.
include("airfoil_aero/AirfoilAero.jl")
include("obj_adapter/ObjAdapter.jl")
include("surfplan_adapter/SurfplanAdapter.jl")

include("precompile.jl")

Expand Down
Loading
Loading