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

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

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

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

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.

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

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.

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

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