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: AeroModel
  • aero_data::AeroData: See AeroData
  • section_aero::Union{Nothing, SectionAero}: optional surface aero table, see SectionAero
source
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 aerodynamics

Distribution Methods

  • LINEAR: Linear interpolation between sections
  • COSINE: Cosine spacing (more panels near tips)
  • SPLIT_PROVIDED: Split each unrefined section into sub-panels
  • UNCHANGED: 1:1 copy when nunrefinedsections == n_panels+1

Keyword Arguments

  • recompute_mapping::Bool=true: Recompute the mapping from refined panels to unrefined sections
  • sort_sections::Bool=true: Sort sections by spanwise position using global LE_point[2] (Y-axis). Disable for structural ordering.

Effects

  1. Populates wing.refined_sections (n_panels+1 sections)
  2. Populates wing.non_deformed_sections (copy of refined_sections for deformation reference)
  3. Computes wing.refined_panel_mapping (panel → unrefined section mapping)
  4. Resizes wing.theta_dist and wing.delta_dist to 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)
source

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.SectionAeroType
SectionAero

Per-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.
source
VortexStepMethod.section_surfaceFunction
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).

source
VortexStepMethod.read_section_aeroFunction
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 in the .dat node order, CSV or Arrow as their suffix says (see read_node_table). Returns nothing if any file is missing. This is the single loader for both provided and generated aero.

source

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_parametersFunction
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
source
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.

source
VortexStepMethod.AirfoilAero.LeastSquaresFitType
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.

source
VortexStepMethod.AirfoilAero.ShrinkWrapType
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 one cell_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 has 2*n_points - 1 points, 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; 0 gives plain cosine-in-arclength sampling.
source
VortexStepMethod.AirfoilAero.shrink_wrapFunction
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.

source
VortexStepMethod.AirfoilAero.XFoilSolverType
XFoilSolver

XFoil 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's PANE default).
  • max_iter: viscous iterations per angle (default 100).
  • xtrip: forced transition (upper, lower) as x/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 with Xfoil.pane before solving (default false). The shrink_wrap already 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 — so false tracks NeuralFoil more closely. Set true to let XFoil repanel anyway.
source
VortexStepMethod.AirfoilAero.NeuralFoilSolverType
NeuralFoilSolver

NeuralFoil 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).
source
VortexStepMethod.AirfoilAero.SectionSolutionType
SectionSolution

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. 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).

source
VortexStepMethod.AirfoilAero.deform_sectionFunction
deform_section(x, y, delta; crease_frac=0.9, thickness_frac=1.0,
               flip_thickness_neg=true,
               wrap_method=ShrinkWrap(clearance=0.0))
    -> DeformedSection

Deform 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).

source
VortexStepMethod.AirfoilAero.analyze_sweepFunction
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).

source
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.

source
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 per-node cp on the deformed contour nodes (def.x, def.y) from the predicted edge velocities (neuralfoil_contour_solution). cf is a flat-plate closure (flat_plate_cf), NeuralFoil not exposing skin friction.

source
VortexStepMethod.AirfoilAero.neuralfoil_aeroFunction
neuralfoil_aero(params::KulfanParameters, alpha, Re;
                model_size="xlarge", weights_dir=nothing, kwargs...)

Compute aerodynamic coefficients using NeuralFoil.

Arguments

  • params: Kulfan CST parameters
  • alpha: Angle of attack in degrees (scalar or vector)
  • Re: Reynolds number
  • model_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
source
neuralfoil_aero(x::Vector, y::Vector, alpha, Re; kwargs...)

Convenience function that fits Kulfan parameters from coordinates first.

source
VortexStepMethod.AirfoilAero.deform_kulfanFunction
deform_kulfan(basis, base, upper_deflection, lower_deflection) -> KulfanParameters
deform_kulfan(basis, base, camber) -> KulfanParameters

Add a surface deflection to a fixed set of Kulfan parameters, both deflections sampled on basis.x and normalized by chord. The three-argument form applies one camber deflection to both surfaces, leaving the thickness distribution untouched; the four-argument form deforms the surfaces independently, which is what a double-skin membrane with its own upper and lower control points needs.

Each deflection is first reduced to the part the basis can carry, see chord_residual: the straight line through its own endpoints is a chord rotation and translation, which the basis cannot express and pinv answers with runaway weights. Take that line from chord_line if the frame the deflection was measured in has not already absorbed it.

The leading-edge weight and the trailing-edge thickness are carried over unchanged: they are the two shape freedoms a chord-referenced deflection cannot resolve.

source
VortexStepMethod.AirfoilAero.chord_residualFunction
chord_residual(basis, deflection) -> Vector{Float64}

The part of a deflection the CST basis can represent. C(x) vanishes at both chord ends, so the basis can put neither the leading nor the trailing edge off the chord line: a deflection that does not end at zero is asking for a chord rotation and translation, not a camber change. Projecting it anyway is not merely inexact, it is unstable — pinv answers an unrepresentable end displacement with weights an order of magnitude past the ones it is correcting, and the airfoil that comes back is not one.

Removes the straight line through the deflection's own endpoints, which chord_line returns, and gives back what is left.

source
VortexStepMethod.AirfoilAero.chord_lineFunction
chord_line(basis, deflection) -> (offset, slope)

The straight line through a deflection's own endpoints, both over chord: offset moves the leading edge and slope is the chord rotation atan(slope) [rad] the caller owes its angle of attack, when the frame it measured the deflection in has not already absorbed it. It is the part chord_residual removes.

source
VortexStepMethod.AirfoilAero.control_point_deflectionFunction
control_point_deflection(basis, fractions, deflections) -> Vector{Float64}

Resample a deflection given at arbitrary chord fractions onto basis.x, by linear interpolation, held flat outside the sampled range. This is the generic bridge from control points — beam nodes now, membrane nodes later — to the CST basis: the caller only has to say where along the chord each point sits and how far off the chord line it moved, both normalized by the local chord.

fractions need not be sorted, but must not repeat a station.

source
VortexStepMethod.AirfoilAero.panel_kulfan_parametersFunction
panel_kulfan_parameters(panels; delta=0.0) -> Vector{KulfanParameters}

Fit the undeformed Kulfan parameters of every panel from the surface contour its section_aero carries, the starting point LivePolars deforms.

The contour is shrink_wrapped first, the same route the offline polar generator takes: fitting a raw slice directly returns weights that oscillate by an order of magnitude more than the deformation ever will, and neighbouring panels then disagree enough to cost the VSM solve its convergence. The wrap costs ~15 ms a panel, which is why this is done once at build time and never inside the loop.

source
VortexStepMethod.AirfoilAero.refresh_live_polars!Function
refresh_live_polars!(live, panels, alpha_ref, reynolds; deflection=nothing)
    -> Float64

Regenerate every panel's polar table from its current shape and write it in (see 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 — and so is everything around it: the shapes, the network inputs, both symmetries of the forward pass and the decoded coefficients all land in storage live already holds, so a refresh every solve costs the forward pass and nothing else.

Each panel keeps the deformed shape it was evaluated at as its live_shape, so a plot draws the airfoil the network actually saw.

alpha_ref [rad] and reynolds are per panel or scalar. Every panel and sample goes through the network in one forward pass. Returns the lowest analysis confidence over the batch, a value near zero meaning the deformed shape has left the region the network was trained on.

Call it after the mesh has been rebuilt from the structure and before the solve: a mesh rebuild re-seeds each panel's aero from its section, which would drop the polar.

source
VortexStepMethod.AirfoilAero.deform_live_shapes!Function
deform_live_shapes!(live::LivePolars, deflection) -> Vector{KulfanParameters}

Deform every base airfoil by its camber increment and store the result in live.deformed, which is returned. nothing writes the base shapes back. The deformation is an analytic perturbation of one fixed weight vector, so it never refits and never inherits the non-uniqueness of a fit.

Each entry keeps the object it already was, its weights overwritten, so a panel holding one from an earlier refresh follows the current shape and no frame allocates a new one.

source
VortexStepMethod.AirfoilAero.apply_live_shapes!Function
apply_live_shapes!(live::LivePolars, panels; deflection=nothing)

Write each panel's deformed airfoil into live_shape without touching its polar, so the shape a panel reports is the one its current deformation gives. Replaying a log re-derives the drawn airfoil this way: the frame is never solved, so its polars would say nothing, and the network pass they cost is the whole price of a refresh.

source
VortexStepMethod.AirfoilAero.polar_driftFunction
polar_drift(live, alpha) -> Float64

How far the largest panel angle of attack has drifted off the reference angle its polar was sampled about, as a fraction of the reach the samples give it. Above 1 a panel is being evaluated past the last sample, where the polar is held flat and says nothing about how the panel is really behaving, so this is the diagnostic a caller's solve loop reports or refreshes on. Asymmetric offsets are measured by their shorter side.

source
VortexStepMethod.AirfoilAero.compare_live_polarFunction
compare_live_polar(live::LivePolars, panel, panel_idx, alpha, reynolds;
                   solver=live_xfoil_solver(live)) -> NamedTuple

One viscous solve of panel panel_idx's current deformed shape against the live polar that panel is flying, at the same angle and Reynolds. Returns (; panel_idx, alpha, requested, reynolds, confidence, cl, cd, cm), each coefficient a (live, reference) pair.

XFoil is marched out from zero in ramp_step increments rather than jumped to the angle, and the comparison is made at the nearest angle it converged at — alpha is that angle and requested the one asked for. Both coefficients are read there, so the 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 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 throwing, so a sweep over panels does not stop at the first one that fails.

source
VortexStepMethod.AirfoilAero.refresh_live_pressure!Function
refresh_live_pressure!(cp, live, contour_x, contour_y, leading_edge, alpha,
                       reynolds) -> Vector{Vector{Float64}}

Fill cp[i] with the surface pressure of panel i's current deformed shape at alpha[i], reconstructed on the contour nodes (contour_x[i], contour_y[i]) (see contour_pressure). cp is written in place and returned.

This is the pressure half of a live polar. refresh_live_polars! makes the panel forces follow the deformed shape; without this the pattern that spreads those forces over the structure would still come from the undeformed section, so a deformation would change how hard a panel pulls but not where it pulls.

One batched forward pass over all panels — through the same workspace the polar refresh uses, whose inputs it has already consumed — at the converged angle of attack rather than at the sampled ones — a panel's Cp is wanted at exactly one angle, and evaluating there is both cheaper than storing the samples and exact. Call it after the solve has converged, with the contours the traction pattern is indexed on — their deformed y, since the reconstruction runs in arc length and a deformed nose is a different distance around. Deform the shapes first, which refresh_live_polars! already did for this solve.

source
VortexStepMethod.AirfoilAero.live_surface_friction!Function
live_surface_friction!(cf, contour_x, reynolds) -> Vector{Vector{Float64}}

Fill cf[i] with the skin friction of panel i's contour nodes at reynolds[i], by the same flat-plate closure (flat_plate_cf) the offline tables carry — NeuralFoil does not predict skin friction. It depends on chord fraction and Reynolds only, not on the shape or the angle of attack, so the live value differs from the tabulated one purely by being at the panel's own flight Reynolds instead of the one the tables were generated at.

source
VortexStepMethod.AirfoilAero.contour_shape_matrixFunction
contour_shape_matrix(contour_x, n_weights) -> Matrix{Float64}

The CST shape matrix C(x)·B(x) at chord fractions contour_x, mapping a change in Kulfan weights to the normal offset it produces there. Build it once per contour: the chord fractions of a panel's contour nodes never move, only the weights do.

source
VortexStepMethod.AirfoilAero.live_shape_offset!Function
live_shape_offset!(offset, live::LivePolars, shape) -> Vector{Vector{Float64}}

Fill offset[i] with the normal offset, over chord, that panel i's current deformation adds to its contour, given the panel's shape matrix from contour_shape_matrix. Written in place and returned.

A deflection deforms both surfaces by the same camber increment, so one offset serves the upper and lower halves of a contour alike. Added to a reference contour it gives the surface the network was actually evaluated on, which is what a traction pattern has to be draped over for its normals and segment areas to mean anything.

source
VortexStepMethod.AirfoilAero.generate_aero_matricesFunction
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).

source
VortexStepMethod.AirfoilAero.generate_polar_from_coordinatesFunction
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.

source
VortexStepMethod.AirfoilAero.generate_airfoil_aeroFunction
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!.

source
generate_airfoil_aero(solver, x::Vector, y::Vector; kwargs...) -> (SectionAero, sols)

Convenience: shrink_wrap the coordinates and fit base Kulfan parameters (LeastSquaresFit) first.

source
VortexStepMethod.AirfoilAero.generate_airfoilsFunction
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,
                  table_format=:csv) -> (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.{table_format} / _cf.{table_format} (per-node surface pressure and skin friction, :csv or the far faster-loading :arrow); 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.

source
VortexStepMethod.AirfoilAero.write_section_aeroFunction
write_section_aero(dat_prefix, aero::SectionAero; table_prefix=dat_prefix,
                   table_format=:csv) -> (dat, cp_table, cf_table)

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.{ext} and {table_prefix}_cf.{ext}, where ext is table_format, either :csv (default, readable) or :arrow (binary, an order of magnitude faster to load). Pass a separate table_prefix to keep the surface tables out of the airfoil-shape directory. read_section_aero reads either format back, detecting it from the suffix. The single writer for surface aero (submodule side); loading lives in the main package.

source

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_yamlFunction
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. A tip that tapers to a point carries no airfoil, so the outermost stations stop at the last slice that still has a chord (station_indices); wingtip_distance moves them a further arc length inboard.

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 is reused; force=true regenerates it (e.g. after changing delta_range or the mesh).

geometry_path names the YAML itself, output_dir/geometry.yaml by default. Point it elsewhere to keep the YAML out of the table directory — the emitted table references then carry the path from the YAML's directory to output_dir, which is what the geometry loader resolves them against.

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 a delta_range); {tag} is the deflection in degrees with m for minus and p for the decimal point (e.g. _dm3.dat, _d2p5.dat)
  • polars/{j}.csv — NeuralFoil polar (alpha, Cd, Cs, Cl, Cm)
  • pressure/{j}_cp.{table_format}, _cf.{table_format} — per-node surface pressure and skin friction; table_format is :csv (default, readable) or :arrow (binary, an order of magnitude faster to load)
  • geometry.yamlwing_sections + wing_airfoils referencing the above

Returns

  • Path to the written geometry.yaml.
source
VortexStepMethod.AirfoilAero.write_yamlFunction
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.

source
VortexStepMethod.ObjAdapter.perpendicular_sectionsFunction
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).

Stations closed to a point at the tips are skipped (station_indices), and wingtip_distance insets the outermost sections a further arc length.

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.

source
VortexStepMethod.ObjAdapter.plot_airfoilsFunction
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).

source
VortexStepMethod.ObjAdapter.plot_slices_3dFunction
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.

source

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_yamlFunction
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_path

Generate 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. table_format writes the per-node surface tables as :csv (default, readable) or :arrow (binary, an order of magnitude faster to load).

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.

source

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]

omega is also projected onto each panel's spanwise axis into pitch_rate_dist, which the solver reads when flow_curvature is enabled.

source
set_va!(body_aero::BodyAerodynamics, va_distribution::AbstractMatrix;
        pitch_rate_dist=nothing)

Set a per-panel inflow distribution. pitch_rate_dist gives each panel's rotation rate about its own spanwise axis [rad/s], positive nose-up; build it with section_pitch_rate when the structure deforms, since twist and flapping rates differ per section and no single body rate describes them. It is reset to zero when omitted, because this method takes no omega and a stale one would silently feed the flow_curvature moment.

source
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 modify
  • settings::VSMSettings: Settings object containing flight conditions

Example

settings = VSMSettings("path/to/settings.yaml")
body_aero = BodyAerodynamics([wing])
set_va!(body_aero, settings)
source
VortexStepMethod.section_pitch_rateFunction
section_pitch_rate(delta_va, z_airf, chord)
section_pitch_rate(velocity_leading, velocity_trailing, z_airf, chord)

Rate a section rotates about its own spanwise axis, positive nose-up. delta_va is the trailing minus leading edge apparent wind; apparent wind is wind - velocity, so that is the leading minus trailing edge velocity, hence the reversed order in the four-argument form. Chordwise wind variation enters here too. Builds a pitch_rate_dist for set_va! on a deforming structure, where no single body rate describes every section.

source
CommonSolve.solveFunction
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.

source
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

source
VortexStepMethod.solve_base!Function
solve_base!(solver::Solver, body_aero::BodyAerodynamics, gamma_distribution=nothing;
            log=false)

Converge the circulation distribution and leave it in solver.lr.gamma_new, without turning it into forces. Fills the solver's panel arrays, builds the AIC matrices, starts from gamma_distribution (or an elliptical/zero guess when it is nothing or solver.use_gamma_prev is false) and iterates; a LOOP solver that fails to converge retries once with half the relaxation factor. The circulation half of solve!, paired with calc_forces!. Returns nothing.

source
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 not
  • va=[15.0, 0.0, 0.0]: Apparent wind vector
  • omega=zeros(3): Turn rate in kite body frame x y and z

Returns

nothing

source
VortexStepMethod.linearizeFunction
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.

source
VortexStepMethod.calculate_resultsFunction
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.

flow_curvature adds flow_curvature_cm to every section moment, read from body_aero.omega.

Returns: Dict: Results including forces, coefficients and distributions

source

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_geometryFunction
plot_geometry(body_aero::BodyAerodynamics, title; kwargs...)

Plot wing geometry from different viewpoints and optionally save/show plots.

Arguments

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 external pdflatex for rendering (default: false; ignored by Makie)
source
VortexStepMethod.plot_distributionFunction
plot_distribution(y_coordinates_list, results_list, label_list; kwargs...)

Plot spanwise distributions of aerodynamic properties.

Arguments

  • y_coordinates_list: list of spanwise coordinate arrays
  • results_list: list of result dictionaries from solve!
  • 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 external pdflatex for rendering (default: false; ignored by Makie)
source
VortexStepMethod.plot_polarsFunction
plot_polars(solver_list, body_aero_list, label_list; kwargs...)

Plot polar data comparing different solvers and configurations.

Arguments

  • solver_list: list of aerodynamic solvers
  • body_aero_list: list of BodyAerodynamics objects
  • label_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 external pdflatex for rendering (default: false; ignored by Makie)
  • cl_over_cd: plot CL/CD vs angle instead of CL vs CD (default: true)
source
VortexStepMethod.plot_polar_dataFunction
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

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 external pdflatex for rendering (default: false; ignored by Makie)
source
VortexStepMethod.plot_combined_analysisFunction
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 solvers
  • body_aero: BodyAerodynamics object or vector thereof
  • results: results dictionary (or vector) from solve!

Keyword arguments

  • solver_label: label string for the solver (backward-compatible alias for labels)
  • labels: optional label string or vector
  • angle_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 external pdflatex for 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)
source
VortexStepMethod.plot_section_polarsFunction
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: the BodyAerodynamics to plot
  • coefficient: :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")
source

Helper Functions

VortexStepMethod.save_plotFunction
save_plot(fig, save_path, title; data_type=nothing)

Save a Makie figure to a file.

Arguments

  • fig: Makie Figure object
  • save_path: Path to save the plot
  • title: Title of the plot

Keyword arguments

  • data_type: File extension. If nothing, defaults to ".pdf" when the active Makie backend is CairoMakie and ".png" otherwise.
source
VortexStepMethod.show_plotFunction
show_plot(fig; name="", dpi=130)

Display a Makie figure in an interactive session, in the window named name.

Arguments

  • fig: Makie Figure object

Keyword arguments

  • name: Window to draw in; reusing a name reuses its window (default: "")
  • dpi: Dots per inch for the figure (default: 130) - currently unused in Makie
source