plot_geometry,plot_distribution,plot_combined_analysis,plot_section_polars,plot_airfoil_fitandplot_airfoilstakeshow_title=true; withfalsethe title is not drawn, and still names the saved file and window.- The panel plots (
plot!(ax, panel),plot!(ax, body),plot(panel),plot(body)) takeborder_color, default:black, for the panel outlines.
- A refined panel exactly between two unrefined sections maps to the one farther from the middle of the span, so a mirror-symmetric wing maps mirror panels to mirror sections; the first section in order used to win, which puts both midway panels of a three-section wing on the same side.
- The docs define the body frame as KiteUtils'
KAframe: x from LE to TE, y towards the right tip, z = x × y up. The separateKBdefinition, whoseZ = Y × Xpointed down, is gone; the solver's frame is unchanged. CL, CD and CS are documented as wind-axis coefficients,cfx,cfyandcfzas the body-axis ones. - The angle of attack is measured in the plane square to the quarter-chord line, from the chord's projection on that plane (Gaunaa, Li & Pirrung 2026, CP1), everywhere it is formed and where it turns the lift direction. Swept panels read their polars at a different angle than before; unswept panels are unchanged.
stability_derivativesgives the force and moment coefficients and their derivatives with respect to angle of attack, sideslip and the nondimensional roll, pitch and yaw rates p̂ = pb/2V, q̂ = q c_ref/2V, r̂ = rb/2V, turning aboutsolver.reference_point, andtrim_anglethe angles of attack at whichCMychanges sign, with the slope that says whether each trim is stable.apparent_wind(alpha, beta, va)gives the body-frame inflow vector at an angle of attack and sideslip, asset_va!(body_aero, settings)sets it.Solver(settings)andSolver(n_panels, n_unrefined_sections)build a solver without aBodyAerodynamics; keyword arguments override the settings.set_va!(body_aero, va_vec, omega; reference_point)turns the body aboutreference_point[m] instead of the origin. The point is stored onBodyAerodynamics, starts at the origin, and is kept by laterset_va!,reinit!andlinearizecalls until it is given again.- Spanwise-flow viscous drag correction (Gaunaa et al. 2024,
doi:10.1088/1742-6596/2767/2/022068): each section gets a drag increment and a force
along its span from the flow across it, in
solve!,solveandlinearize. Opt-in viais_with_viscous_drag_correction(defaultfalse) on the solver settings. - Force on the chordwise trailed vortex segments bound to each panel (Gaunaa, Li &
Pirrung 2026, doi:10.1088/1742-6596/3224/4/042030): Kutta–Joukowski with the total
velocity at their three-quarter-chord point, in
solve!,solveandlinearize. It lowersCLon a swept, anhedral wing whose sections are perpendicular to the quarter-chord line. Opt-in viais_with_attached_trailed_force(defaultfalse). plot_section_polars(body_aero; panels, alphas, delta)draws cl, cd and cm against α per panel throughcalculate_cl/calculate_cd/calculate_cm, for every aero model and at flap deflectiondelta, in one figure instead of one coefficient per call.linearizetakes aBodyAerodynamicswith more than one wing;theta_idxsanddelta_idxsthen run over the unrefined sections of all wings in order.table_format=:arrowwrites the polars as.arrow, like the per-nodeCp/cftables:write_polarandwrite_polar_matrixwrite Arrow for an.arrowpath andload_polar_datareads it.resolve_aero_geometrytakestable_formattoo, for the polars it generates.obj_to_yamlandsurfplan_to_aero_yamlconvert a reused dataset's polars and node tables totable_format. A CSV polar loads as before when every column is numeric; one with a non-numeric column warns and loads asINVISCID.
- BREAKING:
write_polar_csviswrite_polarandwrite_polar_matrix_csviswrite_polar_matrix. The geometry YAML names the polarpolar_file_path; the old keycsv_file_pathis still read, andobj_to_yamlrewrites it when it migrates a dataset. - BREAKING: artificial damping is removed: the
is_with_artificial_dampingandartificial_dampingkeyword arguments ofSolver, and theartificial_damping,k2andk4solver settings.k2andk4had no effect;artificial_damping: truesmoothed the circulation with fixed factors, so a solve that had it on now gives different results. A settings file that still sets these keys loads with a warning. The post-stall stabiliser isis_with_artificial_viscosity. - BREAKING:
ObjAdapter.center_to_com!,calculate_inertia_tensorandcalc_inertia_y_rotationare removed. Mesh mass properties are computed by SymbolicAWEModels, which reads the mesh withread_faces. - BREAKING: the apparent wind follows one naming rule.
vais its speed [m/s], a_vecsuffix makes it the 3-vector [m/s] in the body frame, and a_distsuffix gives one value per panel, sova_vec_distis a 3-vector per panel.v_relis the local flow a panel sees, induced velocity included. Public names that break:body_aero.vaand theva=keyword ofBodyAerodynamicsandreinit!(nowva_vec),VSMSolution._va_distandva_unrefined_dist(nowva_vec_distandva_vec_unrefined_dist),BodyAerodynamics.v_a_dist(nowv_rel_dist), thelinearizekeywordva_idxs(nowva_vec_idxs), thecalculate_resultskey"va_ref"(now"va_ref_vec"), thev_akeyword ofplot_polarsandplot_combined_analysis(nowva), and the settings keyscondition.wind_speedandairfoil.v_app(nowva). The old names raise an error. - Requires Julia 1.12 or 1.13; 1.10 and 1.11 keep resolving v5.1.1.
- The Makie
plot!methods for aPanelor aBodyAerodynamicsreturn aVector{Makie.AbstractPlot}instead of aVector{Any}; for aBodyAerodynamicsdrawn as flat panels it is one flat list rather than a list per panel. Solver(body_aero; kwargs...)andSolver(body_aero, settings)are deprecated and warn on use; build the solver withSolver(settings)orSolver(n_panels, n_unrefined_sections)instead.shrink_wrapsplits the edges of a closed input to at mostmin(0.01, min_concave_radius/2)chord before wrapping it, so the deflected sections and polarsobj_to_yamlgenerates from Kulfan contours move slightly.- BREAKING:
obj_to_yamlandperpendicular_sectionsspread the sections evenly over the span, measured along the quarter-chord line without its chordwise component, instead of over leading-edge arc length, andwingtip_distanceis that spanwise length. A tip whose leading edge runs aft no longer gathers sections into its last centimetres. The same mesh and settings give different section positions, so a tunedwingtip_distanceand any geometry generated from one have to be redone.march_edgesno longer returnsarclen.
deform_sectionre-wraps a section with the rolling ball it was first wrapped with, passed aswrap_method(also taken bygenerate_airfoils,generate_airfoil_aero,generate_aero_matricesandgenerate_polar_from_coordinates), at zero clearance.shrink_wrapwarns when the contour it returns crosses itself. A thin wrapped section came back with its surfaces crossing.shrink_wrapcuts out the loops itsclearanceoffset makes on a thin canopy, so a V3 section wrapped at the defaultMeshSettingsis a simple closed curve and no longer warns; 6 of 18 crossed themselves.- On a body whose wings span different directions, such as a wing and a vertical fin,
solve!,solveandlinearizetake each panel's lift, drag and side directions from its own wing'sspanwise_direction, not the first wing's.solvecomputeswing_spanandaspect_ratio_projectedfrom the extent of all wings along the first wing's span, through the newcalculate_span(wings, spanwise_direction). plot_combined_analysissaves the figure whenis_saveis true, spans its title over the whole figure, frames the wing geometry, and draws literature polars dashed asplot_polarsdoes.plot_polarsandplot_combined_analysisdraw a CS that is zero up to round-off as a flat line instead of scaling the axis to the round-off noise.perpendicular_sectionsandobj_to_yamlorient each cut by the leading edge through the neighbouring stations, not by the step that reached the station, which near a closing tip grazed the surface and returned an 80%-thick section. Every section moves slightly; regenerate existing geometry to benefit.ELLIPTICinitial circulation works on a body with more than one wing, where it threw anArgumentError: each wing gets an ellipse over its own span, along its ownspanwise_directionand centred on its own mid-span, also for a single wing off y = 0.get_lower_upper, and with it the flap hinge indeform_section, takes the lower and upper surface heights where the contour crossesx = crease_frac. It took the nearest points below and abovey = 0, which on a cambered section put the hinge near the chord line or on the wrong surface. Polars with a flap deflection change slightly. It throws anArgumentErrorfor a contour that crosses that line fewer than twice.write_polarforSectionSolutions,write_polar_matrixandwrite_aero_matrixwrite coefficients at 16 significant digits instead of 4 decimals, so aPOLAR_MATRICEStable carries the drag response to a small flap deflection. Regenerate existing tables to benefit.solve!andsolvethrow aDimensionMismatchnaming both sizes for abody_aerowhose panel or unrefined-section count differs from the solver's, where they failed on a broadcast partway through or silently left section results at zero.set_va!(body_aero, settings)appliescondition.yaw_rateas a turn rate about the body z axis; it was read from the settings file and ignored.- The
VSMSolutiondocstring giveslift_dist,drag_distandpanel_moment_distin the per-unit-span units they hold, [N/m] and [Nm/m], instead of [N] and [Nm]. fit_kulfan_parameterswithLeastSquaresFitdrops singular values below1e-4times the largest and warns when it does, so a contour whose stations crowd into a narrow band of the chord gets bounded weights instead of ones that resample it to 1e4 scale. Fits of well-spread stations are unchanged.- Inside its vortex core,
velocity_3D_trailing_vortex!induces an azimuthal velocity instead of a radial one. Only points within the millimetre-scale Oseen core of a panel's chordwise trailing segment were affected. panel_axestakes the panel normal from the quarter-chord step, so the frame closes asz_airf = x_airf × y_airfandz_airfis square to the bound vortex.alphais measured against that normal, socl,cdandcmwere wrong on panels whose two sections have differently-directed chords — twist, sweep or dihedral, not taper alone.
plot_slices_3don a generated output directory draws the deflected contour for every deflection that was generated, not just positive whole degrees. It built its own_d<degrees>.datfilename tag instead of thedelta_suffixone the files were written under, which spells a minus signmand a decimal pointp, sodelta=-10ordelta=2.5found no.datand drew nothing.generate_airfoilsfits the contour it is handed rather than shrink-wrapping it a second time. Both adapters wrap before they call, with thewrap_methodtheir settings name, so the second wrap discarded that setting and degenerated the Kulfan fit: on the SK100's 45 mesh sections nine came out with.datcoordinates at 1e2 to 1e4 instead of 0..1, and any consumer mapping the mesh onto a structure rejected the geometry.- A deflection the 2D solver returned no contour for is no longer written as an
all-
NaNairfoil.dat:write_section_aeroskips that column, soisfilestays the honest test for a generated deflection.read_section_aerowarns and returnsnothingfor a contour that does not hold itsCptable's nodes, where it used to read theNaNs back and interpolate aNaNairfoil shape at every deflection, 0° included.
solve!(...; throw_on_fail=true)throws aSolveFailurewhen the circulation loop missed the solver's tolerances or the coefficients it assembled are not finite —ForwardDiff.Dualpartials included — so a caller that cannot use a failed solve gets an exception rather than aVSMSolutionit has to inspect. The default is off: a post-stall solve that misses the tolerances still returns itssolver_status == FAILUREsolution.
linearize'sfd_absstep/fd_relstep, the finite-difference step it takes when handedbackend=nothing, default to1e-8instead of1e-3— thesqrt(eps)scale theNONLINNewton Jacobian already perturbs by.
- The
NONLINsolver backtracks along each Newton step instead of always taking it whole, so it converges past stall where the full step used to cycle: on thetest/solver/solver_test_wing.yamlwing at 26.6° it stopped 3.6% belowLOOP's peak circulation and reportedFAILURE, and now lands on the same distribution with a fixed-point residual at machine precision. - The
LOOPsolver tests convergence on the fixed-point residual instead of on the under-relaxed step, sortolis the tolerance it reads rather thanrtol / relaxation_factor. EveryLOOPsolve is now tighter by that factor — 33x at the defaults — which moves coefficients in the last few digits and costs around 1.4x the iterations, and a solve that misses the tolerances is no longer retried at a tolerance its first attempt would have passed. plot_slices_3din audit mode no longer crashes when a generated deflection.datholds no finite coordinates (an all-NaNcontour from a deflection the 2D solver converged at no angle): it skips that overlay and warns, naming the file and whether it was blank or missing.
- A wing's settings carry two optional blocks:
mesh:(MeshSettings— theobj_filethe sections are sliced from, section count, leading-edge marching resolution, the mesh-to-slicer rotation, tip standoff and shrink wrap) andairfoil:(AirfoilSettings— the 2D backend, its transition settings, the α and δ sweeps, the Reynolds reference and the table format). Both are optional, so a settings file that names neither loads unchanged, and an unnamedmesh:block slices and wraps exactly as an unconfiguredobj_to_yamlcall. airfoil_solver,alpha_range,delta_range,reynolds,rotation_matrix,slice_argsandpreview_argsturn a block into the argumentsobj_to_yaml, the section solvers and the live polars already take, andNeuralFoilSolver,XFoilSolver,ShrinkWrapandLivePolarSettingseach take the block they are configured by.solver: xfoilselects the viscous panel code for a whole dataset from the settings file, where the backend used to be a caller's hard-coded choice.- The live polars read the
model_sizeandn_critthe tables were generated at rather than their own defaults, so a wing is not re-solved in flight at different transition settings than it was tabulated at. - Reynolds is stated once:
reynolds(set, wing)takes the air fromsolver_settingsand the reference speed and chord fromairfoil:.
- BREAKING:
shrink_wraptraces the rolling ball exactly — pivoting it around the cloud and offsetting the polygon through the points it touches (pivot_contour) — instead of thresholding and marching-squares-tracing a distance field, so there is no grid resolution left to set.ShrinkWrap'scell_sizeis accordingly namedmin_clearance, still accepted under the old name, and floorsclearanceonly for an open single-membrane cloud; a closed loop keeps its trueclearance, soclearance=0hugs the input and leaves its sharp trailing edge sharp. The wrap sits at exactlyclearancefrom the cloud instead of a cell over it, so a V3 canopy's aft strip comes out2 * clearancethick where the grid gave3.5 * cell_size.min_concave_radiusstops costing anything, having padded the grid in both directions before: one V3 slice at radius 0.4 drops from 173 ms to 10 ms, and at the default radius from 15 ms to 3 ms.
- A live polar refresh writes into the panel's own storage instead of rebuilding
its interpolations.
Interpolationshands back a gridded interpolation that reads the very arrays it was given when it is built withinterpolate!, so once a panel's table reads its own knots and values, writing them is the whole update:set_polar!on an unchanged table shape allocates nothing and leaves the interpolation objects in place. A table that changes shape is rebuilt once. refresh_live_polars!now allocates nothing at all, where a refresh over 60 panels cost 15.3 MB before. On one core that buys little — the pass is arithmetic-bound, not allocation-bound — but a sweep running one model per core was spending half its wall time in the garbage collector, which stops every worker. Eight workers refreshing concurrently went from 4.49 to 2.02 ms a refresh, and the collector from 51% of the run to nothing. NeuralFoil's forward pass was the bulk of the garbage: every layer built a fresh matrix for its product, another for its activation, and the symmetry embedding did the whole thing twice more for the flipped case.NeuralFoilWorkspaceholds one set of layer activations per symmetry andfused_output!runs the pass through them, with the bias and activation folded into one in-place sweep, the Mahalanobis penalty subtracted from the confidence logit case by case rather than through a vector of distances, and the flip undone while averaging instead of into a third matrix.LivePolarskeeps that workspace, the network itself, and a buffer for every per-panel quantity a refresh touches, so the shapes deform in place (deform_kulfan!), the coefficients decode in place (decode_coefficients!) and the panel takes them as views.polar_drift,live_surface_friction!andlive_shape_offset!allocate nothing either.refresh_live_pressure!allocates 96% less (3.7 MB to 146 kB a refresh over 60 panels): it shares the polar refresh's workspace, reads the edge velocities out of it into storage it holds, and reconstructs each panel'sCpthrough one set of reused buffers (contour_arc!,contour_pressure!). What is left is the monotone interpolation object itself, one per panel.set_polar!no longer allocates when handed views rather than vectors.KulfanParametersis now mutable, so a live polar source rewrites one shape per panel every solve instead of building a new one, and the panel pointing at it follows without being told. As an immutable it was boxed on its way into the panel'slive_shapefield, which cost a refresh 48 bytes a panel even when the shape had not changed; a panel'slive_shapeis now the very object it was sampled from rather than a copy that compares equal to it. Two separately builtKulfanParameterstherefore no longer compare===or==on identical contents.
- The NeuralFoil surface pressure is now reconstructed from the predicted edge
velocities in arc length, not by interpolating
Cpin chord fraction. NeuralFoil samples at the cell centres of a uniform grid, so it reports nothing over the first and last1/2Nof chord; the old code extrapolatedCpoff the end of each surface independently. Through a stagnation pointueis linear in arc length whileCpis quadratic, so that extrapolated a parabola through its own turning point: on an inflated section it putCp = -1.60at the leading edge, where it has to approach+1, and the two surfaces disagreed at the trailing edge in violation of the Kutta condition. Signing the lower surface negative makes both surfaces one continuous curve through stagnation, so the nose is interpolated rather than extrapolated, and the trailing edge takes one speed for both surfaces. Integrating that section's surface traction now recovers 98% of NeuralFoil's own reported drag where it previously came out with the wrong sign.neuralfoil_sectionreturnsue_upper/ue_loweralongside the pressures.
- A panel's polar table can be rewritten at run time by
set_polar!, which is what a live polar source does every solve. The knots and values reuse the panel's own storage, and the panel records the range the table covers, so a table generated over a window around one angle of attack is held at its end values rather than extrapolated past them, and the stall-angle scan skips a table that stops short of it. The table keeps the shape the panel was built with, so aPOLAR_MATRICESwing takes one too, at everydeltait spans: a regenerated polar carries its deflection as shape and says the same at each. A rewritten table is an ordinary polar in every other way, with no separate aero model. - Live in-memory polars in
AirfoilAero:KulfanBasisanddeform_kulfandeform a fixed Kulfan fit analytically (a matvec against a constant CST basis, never a refit, which is non-unique);control_point_deflectionresamples a deflection given at arbitrary chord fractions — beam nodes, membrane nodes — onto that basis.chord_residualfirst takes the straight line through the deflection's own endpoints off it, which the basis cannot express and answers with runaway weights;chord_linereturns that line, since its rotation is one the caller may still owe its angle of attack.panel_kulfan_parametersfits each panel's contour once at build time, shrink-wrapped first because a raw slice fits to weights that oscillate far more than any deformation will and leave neighbouring panels disagreeing enough to cost the solve its convergence.LivePolarsandrefresh_live_polars!then evaluate NeuralFoil on a grid of angles per panel in one batched forward pass and write those values in as each panel's polar table, so a chordwise deformation reaches the aerodynamics as a shape change rather than a flap angle. The grid moves with the panel —LivePolarSettingscarries the offsets off its current angle of attack — andpolar_driftreports how far the solve has left it, past which the polar holds its last sampled value instead of extrapolating. - Live surface pressure in
AirfoilAero:refresh_live_pressure!regenerates every panel'sCpfrom its deformed shape in one batched forward pass at the converged angle of attack, resampled onto the panel's own contour nodes bycontour_pressure;live_surface_friction!gives the matching skin friction from the flat-plate closure at the panel's live Reynolds. A caller that spreads panel forces over a surface can now make the pattern track the deformation the forces already do.contour_shape_matrixandlive_shape_offset!move the contour itself by the same camber increment the airfoil was deformed by, so the normals and segment areas a traction is built from are the deformed section's rather than the reference one's — the chord fractions never move, only the offsets, so a surface→point map built on that contour stays valid.decode_surface_velocityandcontour_pressurealso back theNeuralFoilSolvercontour assembly, so the live path and the offline table generator reconstruct pressure the same way. compare_live_polarsolves one panel's current deformed shape in XFoil and puts the answer next to the live polar the panel is flying, at the same angle and Reynolds and underlive_xfoil_solver's matched transition settings. XFoil is marched out from zero rather than jumped to the angle — the viscous march refuses a blunt inflated section otherwise, and on a deformed mid-span panel it refused every one — and the comparison is made at the nearest angle it reached, reported asalphabeside therequestedone. ALivePolarsnow keeps each panel's analysis confidence, so the panel worth asking about is the one to hand. NeuralFoil's confidence scores the input shape rather than the answer, so a deformed section far from its training set reads low whether or not the polar is wrong; this is what tells the two apart.prepare_inputsaccepts one Kulfan shape per case, so a whole wing goes through NeuralFoil in a single forward pass.- A panel carries the deformed airfoil its polar was generated from as
live_shape, and the Makie lofted-airfoil skin draws that object rather than the tabulateddeltacontour when it is there. It is the shape the solver was handed, not a re-derivation, so a deformation bug is visible in the picture.KulfanParametersmoved fromAirfoilAerointo the core module for it; it is still exported from both.
- A
POLAR_VECTORStable of at most 24 angles now scans for its knot rather than bisecting, which is faster at that length and is what a live polar's grid is; longer tables keep the binary search. The knot container is part of a panel's interpolation types, so which one a wing uses is fixed when it is built.
- Shared panel aerodynamics (
src/panel_aerodynamics.jl): the per-panel physics written once as pure, branch-free, number-type-generic functions of the section geometry and the flow —panel_axes,panel_inflow,panel_force_directions,panel_loadsand the small helpers around them.update_panel_properties!,init_pos!,calc_forces!andcalculate_resultsnow call them instead of spelling the algebra out three times, andSymbolicAWEModelstraces the same functions with symbolic arguments to build its equations, so the two packages can no longer drift apart. Panel geometry is unchanged bit for bit; forces, moments and coefficients agree to within 1 ulp, the products having been reassociated.calc_forces!stays zero-allocation. effective_alphaand thedeficiencyargument ofpanel_inflowcarry an unsteady lag (a Wagner indicial deficiency) into the angle the polars are read at, while the geometric angle still turns the force. Unused by the solver, which has no unsteady state; it is shared so a symbolic consumer that does have one reads the same definition.
section_pitch_rategained a three-argument form taking the trailing minus leading edge apparent wind directly. The four-argument form is unchanged.- Norms inside the shared panel aerodynamics are floored (
smooth_norm) rather than guarded by branches, so the expressions are differentiable and traceable. The floor is1e-12m, which moves no reported quantity.
- Billowing now takes its rotation axis from the section pair itself instead of
projecting the leading-edge span vector on
spanwise_direction. Sections run+yto-y, so the pair already fixes the sign. The projection was decided by 3 % of the vector on a C-shaped kite's tip panels — 11 mm of span between the outermost sections — so a deforming tip could flip that one panel's billow.
- Section twist now rotates the chord fully about the spanwise axis. The axial
Rodrigues term was missing, so a swept or dihedral section's along-span chord
component was scaled by
cos(theta): the chord shortened and tilted out of the twist plane, growing astheta^2.
table_formatkeyword onwrite_section_aero,generate_airfoils,obj_to_yamlandsurfplan_to_aero_yaml::csv(default, readable) or:arrow(binary, ~40× faster to load and 2.6× smaller).read_section_aerodetects the format from the file suffix, so a geometry YAML can reference either.geometry_pathkeyword onobj_to_yaml, naming the geometry YAML itself instead of always writingoutput_dir/geometry.yaml. Point it outside the table directory and the emitted table references carry the path from the YAML's directory tooutput_dir, which is what the geometry loader resolves them against — so a generated dataset can keep its bulk in a subdirectory while the geometry sits with the hand-written ones.convert_node_tableandwrite_node_rowsrewrite a per-node table in the format the destination suffix names.obj_to_yamlmigrates an existing dataset with them whentable_formatdiffers from what the directory holds, so a dataset changes format without re-running the airfoil solver that produced it.flow_curvaturesolver setting (defaultfalse). When enabled, each section gets the thin-airfoil pitch-rate moment incrementΔcm = -(π/4) q̂withq̂ = q c / (2 v_rel). A section rotating about its own spanwise axis sees an incidence that varies linearly along the chord, which is equivalent to parabolic camber and produces a quarter-chord moment that a single control point cannot represent. The lift response toqwas already exact because the inflow is sampled at the three-quarter-chord point, so only the moment was missing.pitch_rate_distfield onBodyAerodynamics, holding theqabove per panel.set_va!(body_aero, va, omega)fills it by projectingomegaonto every panel'sy_airf, so panels at different dihedral see different rates from one body rate. The distributedset_va!(body_aero, va_distribution; pitch_rate_dist)takes it directly, so twist and flapping rates of a deforming wing — which no single body rate can express — reach the moment. Omitting the keyword zeroes it rather than reusing a staleomega, which the distributed form never sets.section_pitch_rate(velocity_leading, velocity_trailing, z_airf, chord)builds one entry of that distribution from a section's edge velocities, and reduces toω ⋅ y_airffor rigid motion.bin/update_default_manifestsregenerates both checked-in default manifests in one command, andbin/installtakes+X.Y/--version X.Yto pick a Julia channel without prompting. Selecting a version no longer moves the juliaup default, so regenerating the 1.11 manifest leaves the shell on whatever channel it was using.
- The
Solverdocstring quoted theSolverSettingsdefaults fortype_initial_gamma_distributionandcore_radius_fraction(ELLIPTIC,1e-20) instead of its own (ZEROS,0.05), and never said whatcore_radius_fractionmeasures. It now documents theSolverdefaults and cites Damiani et al. (2019) for the 0.05 cut-off. - A remesh under
use_prior_polarno longer resamples the refined sections'SectionAerosurface tables down to whatever unrefined sections survive it.compute_refined_section_interpolation!reblended contour,cpandcffrom the unrefined sections unconditionally whileaero_datawas preserved, so a wing rebuilt onto fewer structural stations kept full-resolution polars but lost the surface tables pressure integration reads. The reblend is now skipped when the polars are preserved and the refined sections already carry tables. - The angle-of-attack correction (
correct_aoa=true) no longer overwritesbody_aero.AICwith the aerodynamic-centre (LLT) matrix. It builds that matrix in its ownAIC_aero_centerfield, soAICkeeps holding the control-point matrix the circulation was solved against, which is what anything reading it after a solve expects. Costs a secondn_panels × n_panels × 3buffer. obj_to_yamlno longer places sections on a wingtip that has closed to a point.station_indicesspreads its targets over the stations that still have a chord, so the outermost section lands on the last sliceable one. A V3 mesh sliced with the defaultwingtip_distanceused to put a zero-chord section at each tip, whose polar wasNaNand took the whole solve with it; working around it meant guessing awingtip_distancelarge enough to skip past the tip. That workaround is no longer the default:wingtip_distanceis now0.0, an inset on top of the trim for meshes whose slices just short of the tip are still too thin to analyse.- Wing sections are normalized to
+yto-yorder on load (normalize_span_order!), and byrefine!for wings built throughadd_section!. Panely_airfandz_airffollow the order sections are stored in, so a geometry file written the other way round inverted every panel normal, and a wing whose sections were replaced after its panels were built (a structural remesh) inverted them mid-run, one panel at a time as each crossedspanwise_direction.obj_to_yamlandsurfplan_to_aero_yamlemit that order too; files of either order keep loading the same. - Spanwise distribution plots put
+yon the left, matching that order and the kite seen from the front. - The lofted airfoil skin in
plot_geometrydraws each section's contour on its own panel edge (#256). The edge was picked from the wing's span order rather than from the panel index, so a wing whose sections ran-yto+ygot every rib drawn at its neighbour's station, one tip bare and the other doubled, while the panels themselves rendered correctly.
SolverSettingsnow defaults to the same values asSolver:core_radius_fraction1e-20→0.05andtype_initial_gamma_distributionELLIPTIC→ZEROS. The 0.05 bound vortex core cut-off follows Damiani et al. (2019), "A Vortex Step Method for Nonlinear Airfoil Polar Data as Implemented in KiteAeroDyn", and matches the upstreamawegroup/Vortex-Step-Methoddefault; at1e-20the Biot-Savart singularity guard never engaged. Coefficients are unchanged for well-separated geometry, since the guard only triggers where a control point falls within 5% of a filament length of a bound vortex, and every settings file shipped indata/sets both keys explicitly.BodyAerodynamics.AICis stored as(n_panels, n_panels, 3)instead of(3, n_panels, n_panels), so each component sliceAIC[:, :, k]is contiguous and the induced-velocity products reach BLASgemvinstead of the generic fallback.solve!is 3.1–5.3× faster (n=120, VSM: 26.1 ms → 4.9 ms inviscid, 21.1 ms → 6.0 ms with polars). Code readingAIC[k, i, j]must becomeAIC[i, j, k].- The filament induced-velocity kernels reuse the
r0/lengtheachBoundFilamentalready stores, take the core-radius cutoff from|r1.r0|/|r0|without forming the perpendicular vector, and defer the cross products that only one branch reads. Output is bit-identical;solve!is a further 1.47-1.55x faster. read_node_tableparses into a preallocated matrix instead ofreduce(vcat, …)over a generator, which was quadratic in the row count: ~21× faster on a 16 MB surface table (2.49 s → 0.12 s), benefiting every existing dataset.is_show=truedraws into a window named after the plot title instead of into whichever window the backend last used, so a script showing several plots gets one window each and re-running it redraws them in place.show_plottakes the windownameas a keyword.examples/V3_kite.jlbuilds its wing fromVSMSettingsand adds a second sweep on polars generated fromV3_25.objwith NeuralFoil (NEURALFOIL, on by default), so the plots compare CAD-derived polars against the checked-in CFD tables.bin/run_juliastarts Julia withJULIA_NUM_THREADS=autounless the environment already sets it.
NeuralFoil-based airfoil polar generation via the newAirfoilAerosubmodule (NeuralFoilSolver,XFoilSolver,analyze_section,analyze_sweep,fit_kulfan_parameters,shrink_wrap,ShrinkWrap)ObjAdaptersubmodule: converts a 3D wing.objmesh to the native YAML/CSV geometry format (obj_to_yaml,perpendicular_sections,write_yaml,plot_slices_3d,plot_airfoils)SurfplanAdaptersubmodule:surfplan_to_aero_yamlturns a SurfplanAdapter aero export into the native pressure-ready YAML/CSV geometry (sharedgenerate_airfoilscore withObjAdapter)ObjWing(obj_path[, dat_path]; Re, n_panels, aero_solver, remake, ...)convenience constructor restored for backward compatibility — internally callsObjAdapter.obj_to_yamlthenWing;aero_solverselects the polar backend (defaultNeuralFoilSolver(); passXFoilSolver()for old behavior);remake=false(default) reuses an existinggeometry.yamlinoutput_dirto skip expensive polar generation when onlyn_panelschanges; setremake=trueto force regeneration- Per-section surface aero tables:
SectionAero(full contour + surface pressurecp+ skin frictioncfper node over an(α, δ)grid),section_surface,read_section_aero/write_section_aero; surface aero propagates throughSectionandWingand is spanwise-interpolated duringrefine! POLYaero model for polynomial cl/cd/cm (exported)lei_poly_coeffs(tube_diameter, camber)(exported fromAirfoilAero) returning the Breukels α-polynomial cl/cd/cm coefficients- Li/Gaunaa spanwise artificial viscosity (Li, Gaunaa, Pirrung & Lønbæk,
TORQUE 2026) for the LOOP solver, stabilizing post-stall circulation
distributions that otherwise develop non-physical sawtooth oscillations;
opt-in via
is_with_artificial_viscosity(defaultfalse) andartificial_viscosity_factor(default0.035) on the solver settings crease_fracwing setting (chordwise flap-hinge fraction, default0.75) for drawing the δ-deflected plate/skin, readable from YAMLexamples/V3_neuralfoil.jlandexamples/obj_to_yaml_kite.jl
- BREAKING -
ObjWingpolar generation now uses NeuralFoil (viaObjAdapter.obj_to_yaml) instead of XFoil + a user-supplied.datfile. The constructor signature is backward-compatible (dat_pathis accepted and silently ignored), but the resulting aerodynamic polars will differ numerically from v3.x outputs. To reproduce old XFoil-based polars passaero_solver=XFoilSolver()toObjAdapter.obj_to_yamland callWingdirectly. - OBJ-based wings are now built via
ObjAdapter.obj_to_yaml+Wing(yaml_path)instead of the oldObjWingpipeline (XFoil + single.datfile);ObjWingis kept as a shim that accepts but ignoresdat_path - BREAKING -
LEI_AIRFOIL_BREUKELSis now a deprecated alias ofPOLY, and the built-in Breukels regression no longer runs at solve time. Sections must carry the α-polynomial coefficients directly instead of(tube_diameter, camber); compute them up front withlei_poly_coeffs(tube_diameter, camber) Sectionandadd_section!accept an optionalsection_aeroargument- BREAKING - plotting is now Makie-only; the
VortexStepMethodMakieExtextension loads once a Makie backend andMakieControlPlotsare available, so plotting code must now loadMakieControlPlots(and dropset_plot_backend!);plot_section_polarsis rendered throughMakieControlPlots
ObjWingas a standalone pipeline (replaced byObjAdapter); the name is re-exported as a compatibility wrapperauto_rotationhelper (internal, removed from public API)PanelGroupingMethodenum (already removed in v3.0.0 — stale docs entry cleaned up)- ControlPlots test run removed from CI (
plot-controlplotsarg) due to alibraqm/HarfBuzz symbol conflict in the GitHub Actions environment - BREAKING - the
ControlPlotsplotting extension,examples_cp/, and thePythonCall/Matplotlib setup (bin/install_controlplots, CondaPkgLocalPreferencesdefaults) - BREAKING - the
PlotBackend/MakieBackend/ControlPlotsBackendtypes andset_plot_backend!; plotting works as soon as a Makie backend andMakieControlPlotsare loaded - the never-implemented
plot_circulation_distribution
calc_forces!andsolve_base!(both exported):solve!is nowsolve_base!followed bycalc_forces!, so a frozen circulation can be mapped to forces without re-running the nonlinear gamma solve (#245)calculate_cdandcalculate_cm, splitting the combinedcalculate_cd_cminto separate drag- and moment-coefficient functions;calculate_cd_cmis kept as a thin wrapper (#246)
calc_forces!is now allocation-free in the per-step hot path (preallocatedpanel_area_distandunrefined_count_distbuffers) (#245)
- 3D polar plotting (#245)
- flaky Aqua
persistent_taskstest now actually disabled viapersistent_tasks=false(()did not disable it) (#246)
moment_coeff_unrefined_distfield inVSMSolution: the summedmoment_frac-referenced pitching-moment coefficient per unrefined section [-]
PlotBackend,MakieBackend,ControlPlotsBackend, andset_plot_backend!so applications can explicitly choose which plotting extension the backend-agnostic plotting API should usePythonCalladded as a weak dependency to support theControlPlotsbackend with PythonPlot
- backend-agnostic plotting wrappers now route through the active plotting backend, and each plotting extension initializes itself as the default only when no backend has been selected yet
- relaxed
ControlPlotscompatibility to include both0.2.5and0.3 - improved
bin/installandbin/install_controlplotsscripts
- corrected projection onto core radius in
velocity_3D_bound_vortex!and semi-infinite trailing vortex projection so that the radial direction is always measured from the filament axis, not from the origin (#241) - fixed 0-based subplot indexing in
ControlPlotsExtfor PythonPlot compatibility (plot_distributionno longer errors with PythonPlot backend) - fixed missing initialization of
dampinsolver.jl
MakieExtandControlPlotsExtno longer both defineVortexStepMethod.plot_geometryfor the same type, resolving a method ambiguity when both extensions were loaded (#236)menu()andmenu_cp()now always set the active backend before dispatching to a plot function, preventing stale-backend errors
- use 2-arg version of atan to avoid possible NaN
unrefined_deform!linearly interpolates twist and TE deflection between unrefined sections and rotates each refined section about the average of its adjacent local airfoil normals (#234)
smooth_sqrtin the solver hot loop keeps gradients defined at zero velocity magnitude
ForwardDiffcompatibility, used by default inlinearize(#232)backendkeyword argument forlinearize- example
linearize_check.jlcomparing FiniteDiff and ForwardDiff tangents
- core structs are parameterized on the scalar type
Tso dual numbers can propagate through them; public constructors are unchanged
- support for both CairoMakie and GLMakie in the example
menu()via newCairoMakie.activate()/GLMakie.activate()entries (#222) - plots are saved as PDF when CairoMakie is active
bin/installnow creates theoutputfolder used by examples to store generated plots
- examples write plots into the shared
outputfolder instead of the working directory - example plot file names are sanitized: spaces replaced with
_and%replaced withpct
- NONLIN solver no longer returns stale
gammaon repeatedsolve!calls (#228)
- bug in
linearizewherebody_aero.vawas used instead ofbody_aero._va, causing incorrect initial velocity storage (#227) set_va!no longer overwrites_vawith a computed reference velocity when omega is nonzero; this simplifies the function and fixes linearization with turn rates- linearize now correctly preserves and restores
omegaacross perturbations
- add back compat entry v2 for SciMLBase
- add back compat entry v3 for RecursiveArrayTools
billowing_anglereplaced bybillowing_percentageonWingandWingSettings(percentage of arc length, not radians)billowing_angle_from_percentage()removedBILLOWINGdistribution now uses sinusoidal rotation instead of circular arc
billowing.jlexample comparing flat vs billowed V3 kite- Coarse V3 kite geometry, settings, and combined CFD polar data
cl_over_cdkeyword forplot_polarsandplot_combined_analysis- the function
menu_cp()can now be used to run the examples with the ControlPlots backend - the script
bin/installcan and should be used to instantiate the project and all sub-projects after git checkout - the files
Manifest-v1.11.toml.defaultandManifest-v12.toml.defaultfor enhanced reproducibility - the scripts
bin/jetlsandbin/jetls_examplesfor running a static check of the source code - the script
test_bench.jlfor benchmarking the refinement method and measuring allocations
- Plot legends moved to shared horizontal legend at bottom of grid layouts
- the script bin/run_julia now can be called with a script name as parameter
- fixed all JETLS warnings in the source code for improved performance and stability
- Domain error in elliptical gamma distribution when control points lie outside the nominal span envelope
- the file
CITATION.cff - compat entry for RecursiveArrayTools
- the file
.zenodo.json
WingandRamAirWingmerged into a singleWingstructRamAirWingconstructor replaced byObjWing(returns aWingwith OBJ-specific fields populated)- Renamed
ram_geometry.jl→obj_geometry.jl
n_groupsreplaced byn_unrefined_sectionsinWing,WingSettings, andSolverSettingsPanelGroupingMethodenum removed- Each refined panel maps back to its unrefined section via
refined_panel_mapping n_groupsandgrouping_methodin YAML settings files emit a deprecation warning and are ignored
panel_width_array→width_distalpha_array→alpha_distcl_array→cl_dist,cd_array→cd_dist,cm_array→cm_distpanel_lift→lift_dist,panel_drag→drag_distpanel_moment→panel_moment_distgroup_moment_dist→moment_unrefined_dist- Type parameter
Solver{P,G}/VSMSolution{P,G}changed toSolver{P,U}/VSMSolution{P,U}(U = unrefined sections)
reinit!no longer re-refines the mesh; it only updates panel properties from existingrefined_sections- New
refine!function handles mesh refinement explicitly
- Consistent naming convention:
_dist= per-panel,_unrefined_dist= per-unrefined-section correct_aoasolver setting added (defaultfalse)alpha_geometricis now the angle between apparent wind and chord- Panel
widthcalculation uses sum instead of average src/plotting.jlremoved; all plotting moved to Makie and ControlPlots extensionsTestEnvno longer required as a global dependency
n_unrefined_sectionsfield inWingfor tracking pre-refinement sectionsrefined_panel_mappingfor automatic panel-to-section association- Unrefined distribution fields in
VSMSolution:cl_unrefined_dist,cd_unrefined_dist,cm_unrefined_dist,alpha_unrefined_dist,moment_unrefined_dist,width_unrefined_dist alpha_geometric_distfield inVSMSolutionunrefined_deform!for applying twist and TE deflection to unrefined sections;non_deformed_sectionspreserved for reset/re-deformationrefine!function for explicit mesh refinementplot_combined_analysisin the Makie extension- Literature comparison plotting in the Makie extension
obj_fileanddat_filefields inWingSettingssort_sectionskwarg for section ordering control- Separate
test/Project.tomlandexamples/Project.toml - New tests:
test_unrefined_dist.jl,test_refinement_validation.jl,test_yaml_wing_deformation.jl
RamAirWingtype (useObjWingorWingdirectly)PanelGroupingMethodenumn_groupsfromWingSettings,SolverSettings, and YAML filessrc/plotting.jl(920 lines moved to extensions)
- A Makie plotting extension.
- Reference in vsm_settings.yaml now points to aero_geometry.yaml.
- None.
- Bridle configuration data (nodes, lines, connections).
- Renamed wing_geometry_polars_CFD.yaml → aero_geometry.yaml.
- The kwarg
aero_coeffsto the functionlinearize: if true the linearization will output normalized coefficients instead of moments and forces.
- New file:
yaml_geometry.jl(290+ lines) — Complete YAML-based wing geometry loading. - New structs:
WingSectionData,WingAirfoilData,WingAirfoilInfo(with@with_kwmacros). - New function:
load_polar_data()— Robust CSV polar data loading with error handling. - New constructors:
Wing(geometry_file::String)— Create wings from YAML files.Wing(settings::VSMSettings)— Create wings from settings.
- Renamed
vs()toVSMSettings()constructor. - Added convenience
Solver(body_aero, settings)constructor. - Improved settings structure and validation.
- Split tests: Reorganized from monolithic files to modular structure
(
test/module_name/test_*.jl). - New test module:
yaml_geometrywithtest_load_polar_data.jlandtest_wing_constructor.jl. - Test utilities:
test_data_utils.jlwith shared helper functions. - Test data: Extensive YAML and CSV test files in
data.
- New data sets: Complete
TUDELFT_V3_KITEwith CFD polars and literature results. - Enhanced examples: Updated examples to use YAML geometry (e.g.,
pyramid_model.jl,V3_kite.jl). - Real-world configs: Production-ready YAML geometry files for various kite configurations.
- Path handling: Robust file path resolution for relative/absolute paths.
- Error handling: Comprehensive validation and graceful fallback to INVISCID mode.
- Memory management: Improved file I/O and cleanup in tests.
- Documentation: Added comprehensive docstrings and examples.
- bump Interpolations to 0.16
- breaking: rename
init!toreinit!
- update NonlinearSolve.jl
- suppress
@infomessages when creating a RamAirWing - improve examples
ram_air_kite.jlandbench.jl
- implement export of
solveandsolve!correctly - do not export
menu()because KiteUtils exports it - the polars of the ram air kite are now saved in
.csvfiles - update CI.yml
- expose the angle of attack
alpha_arrayin theVSMSolution#167
- the parameter
prnto the constructorRamAirWingwhich allows it suppress info messages
- Add back
bench2.jland rename it tobench_solve.jl#150
- Remove problematic parallel Xfoil computing and use single thread instead #161
- The fields that had as type a
Matrix of size Px1have now the typeVector - Many new fields of the type
VSMSolutiondocumented reinit!(body_aero)is now a public function
- Added the option to use nonlinear solve to calculate the gamma distribution #140
- New page
Tips and tricksadded to the documentation - Fast and modular linearization added around an operating point #140
- The function
install_examples()which allows to easily install the examples without usinggit - The function
solve!returns a struct now. The functionsolvethat returns a dict is still available. - The moment coefficients distribution in
solve! - The script
installto thebinfolder for users who checked out this git repository - The script
bench2.jlwas added for allocation testing of thesolve!function
- Read the y-coordinates in the correct direction from the
ram_air_kite_body.objfile - In the
menu.jl, changedhelptohelp_me. It works better now, no more warnings on Linux, it should also work on MacOS now - The coordinate frames of the panels now use the same convention as the kite body frame
- The page "Glossary" of the documentation is quite complete now
- The center of mass field of the
RamAirWingis removed, and the geometry is created such that[0, 0, 0]is the center of mass - The enumeration
WingTypewas added and replaces the symbols, used before - The allocations of the function
solve!where reduced by a factor of 11 to 85 allocations align_to_principaloption added toRamAirWingdeform!by a distribution instead of just a left and right angle
- The function
calculate_circulation_distribution_elliptical_wing()was never called - Fix the calculation of force coefficients in
solve! - The continues integration scripts (CI.yml) use now separate runs for the test coverage and for the allocation tests.
- Dynamically deform the RamAirWing by twisting the left side and right side, and deforming the trailing edges using deform! #19
- Set turn rate
omega = [omega_x, omega_y, omega_z]in kite body frame using set_va! #49 - Add moment coefficient calculations around specified point to
solve#17 - Add moment distribution of the moment around the local panel y-axes around
user-defined points on the panels to
solve!#90 - Add function
solve!()which returns aVSMSolutionstruct #87 - Add the option to remove the NaNs in
aero_datavectors or matrices using theremove_nankeyword in theWingandRamAirWingconstructors #98
- Add origin argument to
BodyAerodynamicsconstructor #66 - Improve documentation
This project is based on version 1.0 of the Python project Vortex-Step-Method
- implemented in Julia, therefore about 50 times faster
- an importer for
.objwing geometry files was added (see: #10) - automatic creation of polars using Xfoil.jl was added (see: #43)
- a ram-air kite example was added
Umagwas replaced withv_aas variable name for the norm of the apparent wind speed- memory allocations were significantly reduced
- a menu (examples/menu.jl) for running the examples was added
- plotting was moved to an extension #55
- added improved online documentation