Functions for creating the geometry
VortexStepMethod.add_section! — Function
add_section!(wing::Wing, LE_point::PosVector, TE_point::PosVector,
aero_model, aero_data::AeroData=nothing, section_aero=nothing)Add a new section to the wing.
Arguments:
- wing::Wing: The Wing to which a section shall be added
- LE_point::PosVector: PosVector of the point on the side of the leading edge
- TE_point::PosVector: PosVector of the point on the side of the trailing edge
aero_model::AeroModel: AeroModelaero_data::AeroData: See AeroDatasection_aero::Union{Nothing, SectionAero}: optional surface aero table, see SectionAero
VortexStepMethod.refine! — Function
refine!(wing::AbstractWing; recompute_mapping=true, sort_sections=true)Refine the wing aerodynamic mesh from unrefined sections to refined sections.
This function interpolates the wing geometry from a coarse set of unrefined sections to a fine mesh of refined sections (npanels+1 sections) based on the wing's spanwisedistribution setting. It also populates nondeformedsections which enables deformation support via unrefined_deform!.
Required Workflow
Must be called after wing construction and before creating BodyAerodynamics:
wing = Wing("wing.yaml"; n_panels=40) # or a manually built Wing
refine!(wing) # Refine mesh
body_aero = BodyAerodynamics([wing]) # Create aerodynamicsDistribution Methods
LINEAR: Linear interpolation between sectionsCOSINE: Cosine spacing (more panels near tips)SPLIT_PROVIDED: Split each unrefined section into sub-panelsUNCHANGED: 1:1 copy when nunrefinedsections == n_panels+1
Keyword Arguments
recompute_mapping::Bool=true: Recompute the mapping from refined panels to unrefined sectionssort_sections::Bool=true: Sort sections by spanwise position using globalLE_point[2](Y-axis). Disable for structural ordering.
Effects
- Populates
wing.refined_sections(n_panels+1 sections) - Populates
wing.non_deformed_sections(copy of refined_sections for deformation reference) - Computes
wing.refined_panel_mapping(panel → unrefined section mapping) - Resizes
wing.theta_distandwing.delta_distto n_panels
Example
# YAML wing
wing = Wing("wing.yaml"; n_panels=40)
refine!(wing)
body_aero = BodyAerodynamics([wing])
# After refinement, deformation is supported
unrefined_deform!(wing, theta_angles, delta_angles)VortexStepMethod.calculate_span — Function
calculate_span(wing::AbstractWing)Calculate wing span along spanwise direction.
Returns: Float64: Wing span
VortexStepMethod.calculate_projected_area — Function
calculate_projected_area(wing::AbstractWing, z_plane_vector=[0.0, 0.0, 1.0])Calculate projected wing area onto plane defined by normal vector.
Returns: Float64: Projected area
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.
VortexStepMethod.SectionAero — Type
SectionAeroPer-2D-section surface aerodynamics at native airfoil resolution: the closed contour (x, y) per trailing-edge deflection, plus surface pressure cp and skin friction cf per contour node over an (alpha, delta) grid (both radians). Replaces the old chord-slice Cp table. Use section_surface to get the interpolated contour + cp/cf at any (alpha, delta).
Fields:
alpha_range,delta_range: grid axes (radians).x,y: contour node coordinates,n_node × n_delta.cp,cf: surface pressure and skin friction,n_node × n_alpha × n_delta.cp_interp,cf_interp,x_interp,y_interp: per-node interpolants.
VortexStepMethod.section_surface — Function
section_surface(aero::SectionAero, alpha, delta) -> (x, y, cp, cf)Interpolated closed contour (x, y) and per-node surface pressure cp and skin friction cf at (alpha, delta) (radians).
VortexStepMethod.read_section_aero — Function
read_section_aero(dat_file, cp_file, cf_file) -> Union{Nothing, SectionAero}Assemble a SectionAero from the human-readable files: the airfoil contour (dat_file, plus {stem}_{delta_suffix(δ)}.dat per non-zero deflection) and the per-node Cp and cf tables (alpha, delta, n0… in the .dat node order). Returns nothing if any file is missing. This is the single loader for both provided and generated aero.
Airfoil aerodynamics (AirfoilAero)
These live in the AirfoilAero submodule of VortexStepMethod, which converts airfoil coordinates to polars and Cp tables. Load it with using VortexStepMethod.AirfoilAero.
VortexStepMethod.AirfoilAero.fit_kulfan_parameters — Function
fit_kulfan_parameters(x::Vector, y::Vector, method::KulfanFitMethod)
fit_kulfan_parameters(x::Vector, y::Vector; n_weights=8)Fit Kulfan CST parameters to airfoil coordinates with LeastSquaresFit, which fits through the points. The keyword form is shorthand for LeastSquaresFit(; n_weights). For a noisy or open point cloud, wrap it with shrink_wrap first.
Input coordinates are assumed to be in Selig order (TE upper -> LE -> TE lower).
Returns
KulfanParameters: Fitted parameters
fit_kulfan_parameters(x, y, method::LeastSquaresFit)Least-squares fit matching AeroSandbox's get_kulfan_parameters: both surfaces share a single least-squares system with a shared leading-edge weight and a trailing-edge thickness.
VortexStepMethod.AirfoilAero.kulfan_to_coordinates — Function
kulfan_to_coordinates(params::KulfanParameters; n_points=100)Generate airfoil coordinates from Kulfan parameters.
Returns (x, y) coordinates in Selig format (TE upper -> LE -> TE lower).
VortexStepMethod.AirfoilAero.KulfanFitMethod — Type
KulfanFitMethodAbstract supertype for Kulfan-parameter fitting strategies. The concrete method LeastSquaresFit is passed to fit_kulfan_parameters, which dispatches on it. To wrap a noisy or open point cloud into a clean airfoil first, use shrink_wrap and then fit the result.
VortexStepMethod.AirfoilAero.LeastSquaresFit — Type
LeastSquaresFit(; n_weights=8)Least-squares Kulfan fit: solves a single system that fits both surfaces through the points, matching AeroSandbox's get_kulfan_parameters (the parameterization NeuralFoil was trained on). Sensitive to noisy interior points; wrap the cloud with shrink_wrap first when fitting a raw slice.
VortexStepMethod.AirfoilAero.ShrinkWrap — Type
ShrinkWrap(; clearance=0.006, min_concave_radius=0.02, cell_size=0.001,
n_points=120, curvature_weight=0.05)Distance-field shrink wrap: the rolling-ball offset of a raw slice point cloud, extracted as one closed airfoil contour. A grid distance field is thresholded at the rolling-ball radius (bridging cloud gaps and crevices narrower than about twice min_concave_radius), flood-filled, eroded back to clearance, and the resulting level set is traced with marching squares, faired (each pass clamped so the contour keeps clearance) and resampled. Because the contour is parameterized by arclength rather than x, the leading edge comes out genuinely round — the offset of the cloud nose — the blunt trailing edge is capped by an arc of radius clearance, and a single-membrane cloud becomes a thin capsule (at least one cell_size half-thickness).
Fields
clearance: offset the contour keeps outside every cloud point; floored at onecell_size(the grid cannot represent a tighter wrap). Also the radius every convex corner is rounded at.min_concave_radius: rolling-ball radius — concave features narrower than about twice this are bridged by a fillet of roughly this radius; convex geometry is unaffected. Auto-raised so the ball can neither fall through the cloud's largest point gap nor pinch off between points during erosion, so sparse clouds get a correspondingly looser, smoother wrap.cell_size: distance-field grid resolution as a chord fraction; sets the geometric fidelity of the wrap.n_points: output stations per surface; the contour has2*n_points - 1points, cosine-clustered in arclength at the leading and trailing edges.curvature_weight: extra sampling measure per radian of contour turning (chord fraction), concentrating output points into corners so XFoil's spline can follow them;0gives plain cosine-in-arclength sampling.
VortexStepMethod.AirfoilAero.shrink_wrap — Function
shrink_wrap(x, y, method::ShrinkWrap) -> (x, y)Wrap the point cloud (x, y) into a clean closed airfoil in Selig order (TE upper → LE → TE lower), following ShrinkWrap: distance field on a cell_size grid, closing with the rolling ball (min_concave_radius), offset outward by clearance, traced as a single closed contour and resampled to cosine panels in a curvature-weighted arclength measure. The first and last point coincide at the trailing edge (the TE cap is part of the contour). The output stays in the normalized frame of the input cloud (chord slightly longer than 1, nose apex near x = -clearance) and is ready to write as a .dat or fit with LeastSquaresFit.
VortexStepMethod.AirfoilAero.AbstractAirfoilSolver — Type
AbstractAirfoilSolverSupertype for a 2D-airfoil solver backend. Concrete subtypes (XFoilSolver, NeuralFoilSolver) select, by dispatch, how a deformed section is analysed. Add a backend by subtyping this and defining analyze_section (and optionally analyze_sweep); the deformation and Cp resampling are shared.
VortexStepMethod.AirfoilAero.XFoilSolver — Type
XFoilSolverXFoil backend. Loads the deformed coordinates (optionally repaneling them) and reads the pressure distribution from Xfoil.cpdump. Defaults match NeuralFoil's training runs (ncrit=9, max_iter=100, incompressible) so the two backends are comparable.
Fields
npan: XFoil panel count when repaneling (default 160, XFoil'sPANEdefault).max_iter: viscous iterations per angle (default 100).xtrip: forced transition(upper, lower)asx/c(default(0.05, 0.05)). Tripping near the leading edge is a valid NeuralFoil transition input and, unlike free transition, converges on the shrink-wrapped hinge of a deflected section.ncrit: e^N transition criticality (default 9.0, the standard clean-tunnel value).mach: Mach number (default 0.0, incompressible).repanel: repanel the input coordinates withXfoil.panebefore solving (defaultfalse). Theshrink_wrapalready emits smoothed cosine panels, and XFoil's own curvature-attracted repaneling re-clusters a deflected section's hinge crease — which NeuralFoil (analysing the smooth Kulfan fit) never sees — sofalsetracks NeuralFoil more closely. Settrueto let XFoil repanel anyway.
VortexStepMethod.AirfoilAero.NeuralFoilSolver — Type
NeuralFoilSolverNeuralFoil backend. Evaluates the deformed section's Kulfan parameters through the network (vectorized over angle) and reconstructs the surface pressure from the predicted edge-velocity ratios. Fast, differentiable, and guarded by analysis_confidence (carried into SectionSolution.confidence).
Fields
model_size: network size (default "large").n_crit: critical amplification factor (default 9.0).xtr_upper,xtr_lower: forced transition locations (default 1.0).weights_dir: override for the weights directory (default package data).
VortexStepMethod.AirfoilAero.SectionSolution — Type
SectionSolutionResult 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. 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).
VortexStepMethod.AirfoilAero.DeformedSection — Type
DeformedSectionA section deformed by a trailing-edge deflection: its coordinates (x, y) and the LeastSquaresFit kulfan parameters fitted to them. XFoil consumes (x, y) directly, NeuralFoil consumes kulfan — both describe the same shape.
VortexStepMethod.AirfoilAero.deform_section — Function
deform_section(x, y, delta; crease_frac=0.9, thickness_frac=1.0,
flip_thickness_neg=true,
wrap_method=ShrinkWrap(clearance=0.0))
-> DeformedSectionDeform the airfoil coordinates (x, y) by trailing-edge deflection delta (radians) about the crease at crease_frac along the chord (pivoting through thickness at thickness_frac, 1.0 = top surface), then re-shrink_wrap the deflected shape into clean cosine panels and fit LeastSquaresFit Kulfan parameters to it. XFoil consumes the coordinates directly, NeuralFoil the Kulfan parameters.
flip_thickness_neg folds a soft membrane about its lower surface for negative delta. The re-wrap uses zero clearance (it hugs the deflected shape at grid resolution); the rolling-ball wrap bridges the crease with a min_concave_radius fillet instead of the overlapping panels that XFoil's own repaneling can hit there. The wrap runs for every delta including 0, so all deflections share the same node count (2·n_points - 1).
VortexStepMethod.AirfoilAero.analyze_section — Function
analyze_section(solver::XFoilSolver, def, alpha, Re) -> SectionSolutionSingle-angle convenience; delegates to analyze_sweep.
analyze_section(solver::NeuralFoilSolver, def, alpha, Re) -> SectionSolutionSingle-angle convenience; delegates to analyze_sweep.
VortexStepMethod.AirfoilAero.analyze_sweep — Function
analyze_sweep(solver, def, alpha_range, Re) -> Vector{SectionSolution}Analyse a deformed section over alpha_range (radians). Generic fallback maps analyze_section; backends override for efficiency (XFoil reinit sweep, NeuralFoil vectorization).
analyze_sweep(solver::XFoilSolver, def, alpha_range, Re) -> Vector{SectionSolution}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. 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.
analyze_sweep(solver::NeuralFoilSolver, def, alpha_range, Re) -> Vector{SectionSolution}Evaluate all angles (radians) in one vectorized NeuralFoil call on the deformed Kulfan parameters, then assemble a single per-node cp on the deformed contour nodes (def.x, def.y) from NeuralFoil's separate upper/lower surface pressures. cf is a flat-plate closure (flat_plate_cf), NeuralFoil not exposing skin friction.
VortexStepMethod.AirfoilAero.neuralfoil_aero — Function
neuralfoil_aero(params::KulfanParameters, alpha, Re;
model_size="xlarge", weights_dir=nothing, kwargs...)Compute aerodynamic coefficients using NeuralFoil.
Arguments
params: Kulfan CST parametersalpha: Angle of attack in degrees (scalar or vector)Re: Reynolds numbermodel_size: Neural network size (default "xlarge")weights_dir: Directory containing weight files
Keyword Arguments
n_crit: Critical amplification factor (default 9.0)xtr_upper: Upper surface transition location (default 1.0)xtr_lower: Lower surface transition location (default 1.0)
Returns
NeuralFoilResult: CL, CD, CM and analysis confidence
neuralfoil_aero(x::Vector, y::Vector, alpha, Re; kwargs...)Convenience function that fits Kulfan parameters from coordinates first.
VortexStepMethod.AirfoilAero.generate_aero_matrices — Function
generate_aero_matrices(solver, x, y; alpha_range, delta_range, Re,
crease_frac=0.75, remove_nan=true, on_deform=nothing)
-> (cl, cd, cm)Build (alpha × delta) coefficient matrices for a base airfoil given as coordinates (x, y). Each delta deflects the trailing edge (deform_section); the deflected shape is then swept over alpha_range (radians) with solver — any AbstractAirfoilSolver, so this works identically for XFoil and NeuralFoil. Re is the Reynolds number. With remove_nan the (non-converged) NaN entries are interpolated away. on_deform(delta, x, y), if given, is called with each deflected shape's coordinates (e.g. to write a per-deflection .dat).
VortexStepMethod.AirfoilAero.generate_polar_from_coordinates — Function
generate_polar_from_coordinates(x, y, output_path; Re, alpha_range=-180:1:180,
solver=NeuralFoilSolver(), delta_range=nothing,
crease_frac=0.75, dat_prefix=nothing)Sweep solver over the airfoil coordinates (x, y) and write the polar CSV. XFoil uses the coordinates directly; NeuralFoil fits LeastSquaresFit Kulfan parameters (deform_section). Wrap a raw or open single-membrane slice with shrink_wrap before calling this. Pass a NeuralFoilSolver or XFoilSolver to pick the backend. With delta_range === nothing the sweep is over alpha_range only and written as a POLAR_VECTORS CSV (returns the Vector{SectionSolution}); pass a delta_range of trailing-edge deflections to sweep (alpha, delta) and write a long-format POLAR_MATRICES CSV (returns the (cl, cd, cm) matrices). Both angle ranges are in degrees. crease_frac is the chordwise hinge location (0–1) about which each delta_range deflection pivots. With dat_prefix set, each deflected shape is also written to {dat_prefix}_{delta_suffix(δ)}.dat.
VortexStepMethod.AirfoilAero.generate_polar_from_dat — Function
generate_polar_from_dat(dat_path, output_path; Re, kwargs...)Read a .dat airfoil and generate its polar CSV via generate_polar_from_coordinates. This is AirfoilAero's .dat → CSV entry point.
VortexStepMethod.AirfoilAero.generate_airfoil_aero — Function
generate_airfoil_aero(solver, base; alpha_range, delta_range, reynolds_number,
crease_frac=0.9, remove_nan=true) -> (SectionAero, sols)Run one solver sweep of a base airfoil (Kulfan) over the (alpha, delta) grid (radians) and return both the SectionAero (contour + Cp + cf per node) and the raw sols::Vector{Vector{SectionSolution}} (one inner vector per delta). The sols also carry cl/cd/cm, so a caller can write the polar from the same sweep — this is how obj_to_yaml avoids a second sweep. Non-converged points stay NaN and, when remove_nan, are filled per node with interpolate_matrix_nans!.
generate_airfoil_aero(solver, x::Vector, y::Vector; kwargs...) -> (SectionAero, sols)Convenience: shrink_wrap the coordinates and fit base Kulfan parameters (LeastSquaresFit) first.
VortexStepMethod.AirfoilAero.generate_section_aero — Function
generate_section_aero(solver, base_or_coords; kwargs...) -> SectionAeroJust the SectionAero from generate_airfoil_aero (drops the raw sols).
VortexStepMethod.AirfoilAero.generate_airfoils — Function
generate_airfoils(airfoils, output_dir; Re, alpha_range=-180:1:180,
delta_range=nothing, aero_solver=NeuralFoilSolver(),
reuse_valid_airfoils=true, crease_frac=0.75, verbose=true)
-> (airfoil_rows, ok)Run the 2D solver over a set of already-shrink-wrapped airfoils and write the per section files each geometry route references. Shared by the .obj and Surfplan adapters: the front-end supplies the clean airfoils and their leading/trailing-edge placement; this writes the surface pressure/friction tables, polars and airfoil .dats, and returns the wing_airfoils rows plus the ids that solved.
airfoils is a vector of (; id, x_fit, y_fit, x_raw, y_raw): x_fit/y_fit is the wrapped airfoil the solver analyses; x_raw/y_raw the raw points it enclosed.
Writes into output_dir (indexed by id), one directory per file kind: airfoils/{id}.dat (wrapped shape), airfoils/{id}_{delta_suffix(δ)}.dat (per deflection), airfoils/{id}_raw.dat (raw points); pressure/{id}_cp.csv / _cf.csv (per-node surface pressure and skin friction); and polars/{id}.csv (POLAR_VECTORS, or a POLAR_MATRICES grid when delta_range is set). aero_solver selects the backend (NeuralFoilSolver default, XFoilSolver opt-in).
With reuse_valid_airfoils=true an airfoil the solver cannot converge is skipped (the caller maps its sections to the nearest solved id); otherwise it errors.
VortexStepMethod.AirfoilAero.write_section_aero — Function
write_section_aero(dat_prefix, aero::SectionAero; table_prefix=dat_prefix)
-> (dat, cp_csv, cf_csv)Write a SectionAero as human-readable files. The airfoil contours share dat_prefix: {dat_prefix}.dat (contour at delta=0) plus {dat_prefix}_{delta_suffix(δ)}.dat per non-zero deflection. The per-node Cp/cf tables share table_prefix (defaults to dat_prefix): {table_prefix}_cp.csv and {table_prefix}_cf.csv. Pass a separate table_prefix to keep the surface tables out of the airfoil-shape directory. read_section_aero reads them back. The single writer for surface aero (submodule side); loading lives in the main package.
OBJ mesh conversion (ObjAdapter)
These live in the ObjAdapter submodule of VortexStepMethod, which converts a 3D wing .obj mesh to the native YAML/CSV geometry format. Load it with using VortexStepMethod.ObjAdapter.
VortexStepMethod.ObjAdapter.obj_to_yaml — Function
obj_to_yaml(obj_path, output_dir; n_sections, Re, alpha_range=-180:1:180,
aero_solver=NeuralFoilSolver(), wrap_method=ShrinkWrap(),
spanwise_direction=[0.0, 1.0, 0.0], crease_frac=0.75, verbose=true)Convert a 3D wing .obj mesh to the native YAML geometry route.
Stations are placed at equal leading-edge arc-length intervals and sliced perpendicular to the local span (see perpendicular_sections), which keeps the airfoil undistorted near curved tips; each shape is then shrink-wrapped into a clean airfoil and evaluated with aero_solver.
aero_solver selects the 2D-airfoil backend: NeuralFoilSolver (default, fast) or XFoilSolver (viscous panel code); pass aero_solver=XFoilSolver() to use XFoil instead. Each section's polar is written as POLAR_VECTORS.
crease_frac is the chordwise hinge location (0–1) about which each delta_range trailing-edge deflection pivots.
With force=false (default) an existing geometry.yaml in output_dir is reused; force=true regenerates it (e.g. after changing delta_range or the mesh).
wrap_method (ShrinkWrap) wraps each slice's point cloud into a clean closed airfoil, robust both to the noisy interior-structure points (ribs, spars) of a ram-air kite slice that otherwise pull a plain least-squares fit inward, and to the complex, harsh corners of an LEI kite slice; NeuralFoil then fits Kulfan parameters to it and XFoil uses it directly.
A slice whose fitted airfoil is implausibly thick relative to the others (e.g. a near-vanishing wingtip slice that fits to a blob) is flagged as degenerate: a section is degenerate when its fitted max|y| exceeds max_thickness_ratio times the median over all sections. With reuse_valid_airfoils=true (default) each degenerate section reuses the nearest valid section's airfoil shape and polar, while keeping its own leading- and trailing-edge positions, so the station is still placed but carries a sane airfoil; a warning lists which sections were reused. With reuse_valid_airfoils=false the degenerate fit is written as is.
Output
Writes into output_dir, indexed by source-airfoil id j (degenerate sections share a neighbour's id):
airfoils/{j}.dat— shrink-wrapped airfoil coordinates (matches the polar)airfoils/{j}_raw.dat— raw sliced section points (the wrap encloses these)airfoils/{j}_d{tag}.dat— each trailing-edge-deflected shape (with adelta_range);{tag}is the deflection in degrees withmfor minus andpfor the decimal point (e.g._dm3.dat,_d2p5.dat)polars/{j}.csv— NeuralFoil polar (alpha, Cd, Cs, Cl, Cm)geometry.yaml—wing_sections+wing_airfoilsreferencing the above
Returns
- Path to the written
geometry.yaml.
VortexStepMethod.AirfoilAero.write_yaml — Function
write_yaml(path, data)Write data (nested Dicts, vectors, scalars) to path as YAML with every float rounded to millimetre precision (3 decimals) and every leaf list on one line. The single writer for all generated geometry YAMLs, so their formatting stays consistent. Mapping keys are emitted in sorted order.
VortexStepMethod.ObjAdapter.perpendicular_sections — Function
perpendicular_sections(vertices, faces, n_sections; n_bins=60, rotation=I)Extract n_sections airfoil cross-sections following a curved or swept span. The leading edge is marched into n_bins stations (march_edges); the airfoil is built (build_section) at the marched station nearest each equal leading-edge arc-length target. Each section is (; LE_point, TE_point, span_dir, contour3d, x_airfoil, y_airfoil).
The slicer assumes x = chordwise, y = spanwise, z = up. Pass a 3×3 rotation matrix to reorient a mesh stored in another convention before slicing.
VortexStepMethod.ObjAdapter.plot_airfoils — Function
plot_airfoils(geometry_file::String; kwargs...)Plot every airfoil of a YAML wing geometry. The airfoil shapes are read from the .dat files referenced by the geometry's wing_airfoils. Implemented in the VortexStepMethodMakieExt extension (load a Makie backend, e.g. GLMakie).
VortexStepMethod.ObjAdapter.plot_slices_3d — Function
plot_slices_3d(path; n_slices=10, rotation=I, delta=0.0, is_show=true)3D slice diagnostic with a hover 2D airfoil panel. path is either a mesh .obj (live preview: slice and wrap here) or a generated obj_to_yaml output directory (audit: plot the written .dat airfoils the polar pipeline analysed). Implemented in the Makie extension (load Makie/GLMakie); see it for all options.
Surfplan conversion (SurfplanAdapter)
These live in the SurfplanAdapter submodule of VortexStepMethod, which converts the output of the (Python) SurfplanAdapter export into the native, VSM-loadable YAML/CSV geometry format. Load it with using VortexStepMethod.SurfplanAdapter.
VortexStepMethod.SurfplanAdapter.surfplan_to_aero_yaml — Function
surfplan_to_aero_yaml(adapter_dir, output_dir; aero_solver=NeuralFoilSolver(),
wrap_method=ShrinkWrap(), alpha_range=-180:1:180, delta_range=nothing,
Re=nothing, crease_frac=0.75, force=false, verbose=true) -> geometry_yaml_pathGenerate a pressure-ready geometry.yaml (per-node surface cp/cf tables plus polars) from a SurfplanAdapter aero export, so the wing can be flown with the AeroPressure continuous coupling instead of only integrated polars. The Surfplan counterpart of obj_to_yaml: where .obj slices a mesh, this reads the already-clean per-rib airfoil .dat profiles the Python adapter exported.
Reads adapter_dir/aero_geometry.yaml for each section's leading/trailing-edge placement (wing_sections) and each airfoil's .dat path (wing_airfoils info_dict.dat_file_path), shrink-wraps every unique profile, and runs the shared generate_airfoils core with aero_solver (NeuralFoilSolver by default, XFoilSolver opt-in). Sections that share an airfoil generate its tables once. Re defaults to the export's wing_airfoils.reynolds; alpha_range defaults to the full -180:1:180 sweep rather than the export's narrow polar range.
The .txt → adapter-YAML step (the upstream Python SurfplanAdapter) is the documented prerequisite. Load the result with Wing(geometry_yaml_path).
An existing geometry.yaml in output_dir is reused as-is, skipping the expensive polar generation; set force=true to regenerate the polars.
Setting the inflow conditions and solving
VortexStepMethod.set_va! — Function
set_va!(body_aero::BodyAerodynamics, va::VelVector, omega=zeros(MVec3))Set velocity array and update wake filaments.
Arguments
- body_aero::BodyAerodynamics: The BodyAerodynamics struct to modify
va::VelVector: Velocity vector of the apparent wind speed [m/s]omega::VelVector: Turn rate vector around x y and z axis [rad/s]
set_va!(body_aero::BodyAerodynamics, settings::VSMSettings)Set velocity array from VSM settings configuration.
This convenience method extracts flight conditions from VSMSettings and constructs the velocity vector in the body reference frame based on:
- Wind speed from settings.condition.wind_speed
- Angle of attack from settings.condition.alpha (converted from degrees)
- Sideslip angle from settings.condition.beta (converted from degrees)
The velocity vector is constructed as:
- Xb (forward): windspeed * cos(α) * cos(β)
- Yb (right): windspeed * sin(β)
- Zb (down): windspeed * sin(α) * cos(β)
Arguments
body_aero::BodyAerodynamics: The aerodynamic body to modifysettings::VSMSettings: Settings object containing flight conditions
Example
settings = VSMSettings("path/to/settings.yaml")
body_aero = BodyAerodynamics([wing])
set_va!(body_aero, settings)CommonSolve.solve — Function
solve(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution=nothing;
log=false, reference_point=solver.reference_point)Main solving routine for the aerodynamic model. Reference point is in the kite body (KB) frame. See also: solve!
Arguments:
- solver::Solver: The solver to use, could be a VSM or LLT solver. See: Solver
- body_aero::BodyAerodynamics: The aerodynamic body. See: BodyAerodynamics
- gamma_distribution: Initial circulation vector or nothing; Length: Number of segments. [m²/s]
Keyword Arguments:
- log=false: If true, print the number of iterations and other info.
- referencepoint=solver.referencepoint
Returns
A dictionary with the results.
CommonSolve.solve! — Function
solve!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution=solver.sol.gamma_distribution;
log=false, reference_point=solver.reference_point, moment_frac=0.1)Main solving routine for the aerodynamic model. Reference point is in the kite body (KB) frame. This version is modifying the solver.sol struct and is faster than the solve function which returns a dictionary.
Arguments:
- solver::Solver: The solver to use, could be a VSM or LLT solver. See: Solver
- body_aero::BodyAerodynamics: The aerodynamic body. See: BodyAerodynamics
- gamma_distribution: Initial circulation vector or nothing; Length: Number of segments. [m²/s]
Keyword Arguments:
- log=false: If true, print the number of iterations and other info.
- referencepoint=solver.referencepoint
- moment_frac=0.1: X-coordinate of normalized panel around which the moment distribution should be calculated.
Returns
The solution of type VSMSolution
VortexStepMethod.reinit! — Method
reinit!(body_aero::BodyAerodynamics; init_aero, va, omega, refine_mesh, recompute_mapping, sort_sections)Initialize a BodyAerodynamics struct in-place by setting up panels and coefficients.
Arguments
body_aero::BodyAerodynamics: The structure to initialize
Keyword Arguments
init_aero::Bool: Whether to initialize the aero data or notva=[15.0, 0.0, 0.0]: Apparent wind vectoromega=zeros(3): Turn rate in kite body frame x y and z
Returns
nothing
VortexStepMethod.linearize — Function
linearize(solver, body_aero, y; theta_idxs=1:4, delta_idxs=nothing,
va_idxs=nothing, omega_idxs=nothing, aero_coeffs=false,
backend=AutoForwardDiff(), kwargs...)Jacobian of aerodynamic outputs w.r.t. control and kinematic inputs at y. Each *_idxs selects which entries of y map to twist angles (one per unrefined section), trailing-edge deflections (one per unrefined section), apparent wind (vx, vy, vz), and angular rate (ωx, ωy, ωz) respectively.
backend accepts any DifferentiationInterface backend; AutoForwardDiff() (the default) requires solver_type=LOOP. fd_absstep/fd_relstep are forwarded only when the backend is AutoFiniteDiff.
Returns (jac, results, converged) where results is (F, M, moment_unrefined_dist...) — or the corresponding coefficients when aero_coeffs=true — and converged is false (with a warning) if any internal solve missed the solver's tolerances.
VortexStepMethod.calculate_results — Function
calculate_results(body_aero::BodyAerodynamics, gamma_new,
density,
core_radius_fraction, mu,
alpha_dist, v_a_dist,
chord_array, x_airf_array,
z_airf_array,
va_array, va_norm_array,
va_unit_array, panels::Vector{<:Panel},
is_only_f_and_gamma_output::Bool)Calculate final aerodynamic results. Reference point is in the kite body (KB) frame.
Returns: Dict: Results including forces, coefficients and distributions
Main Plotting Functions
The plotting functions are implemented as package extensions. They are available once a Makie backend (GLMakie or CairoMakie) and MakieControlPlots are loaded before VortexStepMethod. The examples use GLMakie.
VortexStepMethod.plot_geometry — Function
plot_geometry(body_aero::BodyAerodynamics, title; kwargs...)Plot wing geometry from different viewpoints and optionally save/show plots.
Arguments
body_aero: theBodyAerodynamicsto plottitle: plot title
Keyword arguments
data_type: file extension for saving (default:".png")save_path: path for saving the graphic (default:nothing)is_save: whether to save the graphic (default:false)is_show: whether to display the graphic (default:false)view_elevation: initial view elevation angle in degrees (default:15)view_azimuth: initial view azimuth angle in degrees (default:-120)use_tex: use externalpdflatexfor rendering (default:false; ignored by Makie)
VortexStepMethod.plot_distribution — Function
plot_distribution(y_coordinates_list, results_list, label_list; kwargs...)Plot spanwise distributions of aerodynamic properties.
Arguments
y_coordinates_list: list of spanwise coordinate arraysresults_list: list of result dictionaries fromsolve!label_list: list of labels for each result
Keyword arguments
title: plot title (default:"spanwise_distribution")data_type: file extension for saving (default:".png")save_path: path to save plots (default:nothing)is_save: whether to save (default:false)is_show: whether to display (default:true)use_tex: use externalpdflatexfor rendering (default:false; ignored by Makie)
VortexStepMethod.plot_polars — Function
plot_polars(solver_list, body_aero_list, label_list; kwargs...)Plot polar data comparing different solvers and configurations.
Arguments
solver_list: list of aerodynamic solversbody_aero_list: list ofBodyAerodynamicsobjectslabel_list: list of labels for each configuration
Keyword arguments
literature_path_list: optional paths to literature data CSV files (default:String[])angle_range: range of angles to analyze in degrees (default:range(0, 20, 2))angle_type:"angle_of_attack"or"side_slip"(default:"angle_of_attack")angle_of_attack: AoA for the polar sweep (default:0.0) [°]side_slip: side slip angle (default:0.0) [°]v_a: apparent wind speed magnitude (default:10.0) [m/s]title: plot title (default:"polar")data_type: file extension for saving (default:".png")save_path: path to save plots (default:nothing)is_save: whether to save (default:true)is_show: whether to display (default:true)use_tex: use externalpdflatexfor rendering (default:false; ignored by Makie)cl_over_cd: plot CL/CD vs angle instead of CL vs CD (default:true)
VortexStepMethod.plot_polar_data — Function
plot_polar_data(body_aero::BodyAerodynamics; kwargs...)Plot polar data (Cl, Cd, Cm) as 3-D surfaces against angle of attack and trailing edge deflection.
Arguments
body_aero: theBodyAerodynamicsto plot (must usePOLAR_MATRICESaero model)
Keyword arguments
alphas: AoA values in radians (default:deg2rad.(-5:0.3:25))delta_tes: trailing edge deflection angles in radians (default:deg2rad.(-5:0.3:25))is_show: whether to display (default:true)use_tex: use externalpdflatexfor rendering (default:false; ignored by Makie)
VortexStepMethod.plot_combined_analysis — Function
plot_combined_analysis(solver, body_aero, results; kwargs...)Create a combined analysis by calling plot_geometry, plot_distribution, and plot_polars in sequence.
Arguments
solver: solver or vector of solversbody_aero:BodyAerodynamicsobject or vector thereofresults: results dictionary (or vector) fromsolve!
Keyword arguments
solver_label: label string for the solver (backward-compatible alias forlabels)labels: optional label string or vectorangle_range: range of angles for polar plots (default:range(0, 20, length=20))angle_type:"angle_of_attack"or"side_slip"(default:"angle_of_attack")angle_of_attack: AoA in degrees (default:0.0)side_slip: side slip angle in degrees (default:0.0)v_a: wind speed in m/s (default:10.0)title: overall figure title (default:"Combined Analysis")view_elevation: geometry view elevation in degrees (default:15)view_azimuth: geometry view azimuth in degrees (default:-120)is_show: whether to display (default:true)use_tex: use externalpdflatexfor rendering (default:false; ignored by Makie)literature_path_list: paths to literature CSV files (default:String[])data_type: file extension for saving (default:".png")save_path: directory to save files (default:nothing)is_save: whether to save (default:false)cl_over_cd: plot CL/CD vs angle (default:true)
VortexStepMethod.plot_section_polars — Function
plot_section_polars(body_aero::BodyAerodynamics, coefficient=:cl; kwargs...)Plot one polar coefficient (:cl, :cd, or :cm) against angle of attack for every section of a wing using stored POLAR_VECTORS data. Rendered through MakieControlPlots.
Arguments
body_aero: theBodyAerodynamicsto plotcoefficient::cl,:cd, or:cm(default::cl)
Keyword arguments
is_show: whether to display (default:true)is_save: whether to save (default:false)save_path: directory to save the figure (default:nothing)data_type: file extension for saving (default:".png")
Helper Functions
VortexStepMethod.save_plot — Function
save_plot(fig, save_path, title; data_type=nothing)Save a Makie figure to a file.
Arguments
fig: Makie Figure objectsave_path: Path to save the plottitle: Title of the plot
Keyword arguments
data_type: File extension. Ifnothing, defaults to".pdf"when the active Makie backend is CairoMakie and".png"otherwise.
VortexStepMethod.show_plot — Function
show_plot(fig; dpi=130)Display a Makie figure.
Arguments
fig: Makie Figure object
Keyword arguments
dpi: Dots per inch for the figure (default: 130) - currently unused in Makie