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
139 changes: 114 additions & 25 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 a `BodyAerodynamics` object by plotting each of its panels.
"""
plot!(ax, body::VortexStepMethod.BodyAerodynamics; use_observables=false,
airfoils=false, kwargs...)

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
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
12 changes: 8 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,9 @@ 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")
include("geometry_yaml.jl")
include("geometry_gen.jl")

export KulfanParameters, KulfanFitMethod, LeastSquaresFit
export ShrinkWrap, shrink_wrap
Expand All @@ -28,9 +30,11 @@ 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
export write_geometry_yaml, write_yaml, generate_airfoils

end
Loading
Loading