Skip to content

Latest commit

 

History

History
1145 lines (933 loc) · 55.9 KB

CHANGELOG.md

File metadata and controls

1145 lines (933 loc) · 55.9 KB

Changelog

VortexStepMethod v6.1.0 2026-10-04

Added

  • plot_geometry, plot_distribution, plot_combined_analysis, plot_section_polars, plot_airfoil_fit and plot_airfoils take show_title=true; with false the 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)) take border_color, default :black, for the panel outlines.

Fixed

  • 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' KA frame: x from LE to TE, y towards the right tip, z = x × y up. The separate KB definition, whose Z = Y × X pointed down, is gone; the solver's frame is unchanged. CL, CD and CS are documented as wind-axis coefficients, cfx, cfy and cfz as 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.

VortexStepMethod v6.0.0 2026-09-23

Added

  • stability_derivatives gives 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 about solver.reference_point, and trim_angle the angles of attack at which CMy changes 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, as set_va!(body_aero, settings) sets it.
  • Solver(settings) and Solver(n_panels, n_unrefined_sections) build a solver without a BodyAerodynamics; keyword arguments override the settings.
  • set_va!(body_aero, va_vec, omega; reference_point) turns the body about reference_point [m] instead of the origin. The point is stored on BodyAerodynamics, starts at the origin, and is kept by later set_va!, reinit! and linearize calls 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!, solve and linearize. Opt-in via is_with_viscous_drag_correction (default false) 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!, solve and linearize. It lowers CL on a swept, anhedral wing whose sections are perpendicular to the quarter-chord line. Opt-in via is_with_attached_trailed_force (default false).
  • plot_section_polars(body_aero; panels, alphas, delta) draws cl, cd and cm against α per panel through calculate_cl/calculate_cd/calculate_cm, for every aero model and at flap deflection delta, in one figure instead of one coefficient per call.
  • linearize takes a BodyAerodynamics with more than one wing; theta_idxs and delta_idxs then run over the unrefined sections of all wings in order.
  • table_format=:arrow writes the polars as .arrow, like the per-node Cp/cf tables: write_polar and write_polar_matrix write Arrow for an .arrow path and load_polar_data reads it. resolve_aero_geometry takes table_format too, for the polars it generates. obj_to_yaml and surfplan_to_aero_yaml convert a reused dataset's polars and node tables to table_format. A CSV polar loads as before when every column is numeric; one with a non-numeric column warns and loads as INVISCID.

Changed

  • BREAKING: write_polar_csv is write_polar and write_polar_matrix_csv is write_polar_matrix. The geometry YAML names the polar polar_file_path; the old key csv_file_path is still read, and obj_to_yaml rewrites it when it migrates a dataset.
  • BREAKING: artificial damping is removed: the is_with_artificial_damping and artificial_damping keyword arguments of Solver, and the artificial_damping, k2 and k4 solver settings. k2 and k4 had no effect; artificial_damping: true smoothed 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 is is_with_artificial_viscosity.
  • BREAKING: ObjAdapter.center_to_com!, calculate_inertia_tensor and calc_inertia_y_rotation are removed. Mesh mass properties are computed by SymbolicAWEModels, which reads the mesh with read_faces.
  • BREAKING: the apparent wind follows one naming rule. va is its speed [m/s], a _vec suffix makes it the 3-vector [m/s] in the body frame, and a _dist suffix gives one value per panel, so va_vec_dist is a 3-vector per panel. v_rel is the local flow a panel sees, induced velocity included. Public names that break: body_aero.va and the va= keyword of BodyAerodynamics and reinit! (now va_vec), VSMSolution._va_dist and va_unrefined_dist (now va_vec_dist and va_vec_unrefined_dist), BodyAerodynamics.v_a_dist (now v_rel_dist), the linearize keyword va_idxs (now va_vec_idxs), the calculate_results key "va_ref" (now "va_ref_vec"), the v_a keyword of plot_polars and plot_combined_analysis (now va), and the settings keys condition.wind_speed and airfoil.v_app (now va). 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 a Panel or a BodyAerodynamics return a Vector{Makie.AbstractPlot} instead of a Vector{Any}; for a BodyAerodynamics drawn as flat panels it is one flat list rather than a list per panel.
  • Solver(body_aero; kwargs...) and Solver(body_aero, settings) are deprecated and warn on use; build the solver with Solver(settings) or Solver(n_panels, n_unrefined_sections) instead.
  • shrink_wrap splits the edges of a closed input to at most min(0.01, min_concave_radius/2) chord before wrapping it, so the deflected sections and polars obj_to_yaml generates from Kulfan contours move slightly.
  • BREAKING: obj_to_yaml and perpendicular_sections spread the sections evenly over the span, measured along the quarter-chord line without its chordwise component, instead of over leading-edge arc length, and wingtip_distance is 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 tuned wingtip_distance and any geometry generated from one have to be redone. march_edges no longer returns arclen.

Fixed

  • deform_section re-wraps a section with the rolling ball it was first wrapped with, passed as wrap_method (also taken by generate_airfoils, generate_airfoil_aero, generate_aero_matrices and generate_polar_from_coordinates), at zero clearance. shrink_wrap warns when the contour it returns crosses itself. A thin wrapped section came back with its surfaces crossing.
  • shrink_wrap cuts out the loops its clearance offset makes on a thin canopy, so a V3 section wrapped at the default MeshSettings is 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!, solve and linearize take each panel's lift, drag and side directions from its own wing's spanwise_direction, not the first wing's. solve computes wing_span and aspect_ratio_projected from the extent of all wings along the first wing's span, through the new calculate_span(wings, spanwise_direction).
  • plot_combined_analysis saves the figure when is_save is true, spans its title over the whole figure, frames the wing geometry, and draws literature polars dashed as plot_polars does.
  • plot_polars and plot_combined_analysis draw a CS that is zero up to round-off as a flat line instead of scaling the axis to the round-off noise.
  • perpendicular_sections and obj_to_yaml orient 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.
  • ELLIPTIC initial circulation works on a body with more than one wing, where it threw an ArgumentError: each wing gets an ellipse over its own span, along its own spanwise_direction and centred on its own mid-span, also for a single wing off y = 0.
  • get_lower_upper, and with it the flap hinge in deform_section, takes the lower and upper surface heights where the contour crosses x = crease_frac. It took the nearest points below and above y = 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 an ArgumentError for a contour that crosses that line fewer than twice.
  • write_polar for SectionSolutions, write_polar_matrix and write_aero_matrix write coefficients at 16 significant digits instead of 4 decimals, so a POLAR_MATRICES table carries the drag response to a small flap deflection. Regenerate existing tables to benefit.
  • solve! and solve throw a DimensionMismatch naming both sizes for a body_aero whose 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) applies condition.yaw_rate as a turn rate about the body z axis; it was read from the settings file and ignored.
  • The VSMSolution docstring gives lift_dist, drag_dist and panel_moment_dist in the per-unit-span units they hold, [N/m] and [Nm/m], instead of [N] and [Nm].
  • fit_kulfan_parameters with LeastSquaresFit drops singular values below 1e-4 times 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_axes takes the panel normal from the quarter-chord step, so the frame closes as z_airf = x_airf × y_airf and z_airf is square to the bound vortex. alpha is measured against that normal, so cl, cd and cm were wrong on panels whose two sections have differently-directed chords — twist, sweep or dihedral, not taper alone.

VortexStepMethod v5.1.1 2026-09-12

Fixed

  • plot_slices_3d on 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>.dat filename tag instead of the delta_suffix one the files were written under, which spells a minus sign m and a decimal point p, so delta=-10 or delta=2.5 found no .dat and drew nothing.
  • generate_airfoils fits the contour it is handed rather than shrink-wrapping it a second time. Both adapters wrap before they call, with the wrap_method their 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 .dat coordinates 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-NaN airfoil .dat: write_section_aero skips that column, so isfile stays the honest test for a generated deflection. read_section_aero warns and returns nothing for a contour that does not hold its Cp table's nodes, where it used to read the NaNs back and interpolate a NaN airfoil shape at every deflection, 0° included.

VortexStepMethod v5.1.0 2026-09-11

Added

  • solve!(...; throw_on_fail=true) throws a SolveFailure when the circulation loop missed the solver's tolerances or the coefficients it assembled are not finite — ForwardDiff.Dual partials included — so a caller that cannot use a failed solve gets an exception rather than a VSMSolution it has to inspect. The default is off: a post-stall solve that misses the tolerances still returns its solver_status == FAILURE solution.

Changed

  • linearize's fd_absstep/fd_relstep, the finite-difference step it takes when handed backend=nothing, default to 1e-8 instead of 1e-3 — the sqrt(eps) scale the NONLIN Newton Jacobian already perturbs by.

Fixed

  • The NONLIN solver backtracks along each Newton step instead of always taking it whole, so it converges past stall where the full step used to cycle: on the test/solver/solver_test_wing.yaml wing at 26.6° it stopped 3.6% below LOOP's peak circulation and reported FAILURE, and now lands on the same distribution with a fixed-point residual at machine precision.
  • The LOOP solver tests convergence on the fixed-point residual instead of on the under-relaxed step, so rtol is the tolerance it reads rather than rtol / relaxation_factor. Every LOOP solve 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_3d in audit mode no longer crashes when a generated deflection .dat holds no finite coordinates (an all-NaN contour 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.

VortexStepMethod v5.0.0 2026-09-07

Added

  • A wing's settings carry two optional blocks: mesh: (MeshSettings — the obj_file the sections are sliced from, section count, leading-edge marching resolution, the mesh-to-slicer rotation, tip standoff and shrink wrap) and airfoil: (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 unnamed mesh: block slices and wraps exactly as an unconfigured obj_to_yaml call.
  • airfoil_solver, alpha_range, delta_range, reynolds, rotation_matrix, slice_args and preview_args turn a block into the arguments obj_to_yaml, the section solvers and the live polars already take, and NeuralFoilSolver, XFoilSolver, ShrinkWrap and LivePolarSettings each take the block they are configured by.
  • solver: xfoil selects 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_size and n_crit the 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 from solver_settings and the reference speed and chord from airfoil:.

Changed

  • BREAKING: shrink_wrap traces 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's cell_size is accordingly named min_clearance, still accepted under the old name, and floors clearance only for an open single-membrane cloud; a closed loop keeps its true clearance, so clearance=0 hugs the input and leaves its sharp trailing edge sharp. The wrap sits at exactly clearance from the cloud instead of a cell over it, so a V3 canopy's aft strip comes out 2 * clearance thick where the grid gave 3.5 * cell_size. min_concave_radius stops 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.

VortexStepMethod v4.3.1 2026-09-01

Changed

  • A live polar refresh writes into the panel's own storage instead of rebuilding its interpolations. Interpolations hands back a gridded interpolation that reads the very arrays it was given when it is built with interpolate!, 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. NeuralFoilWorkspace holds one set of layer activations per symmetry and fused_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. LivePolars keeps 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! and live_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's Cp through 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.
  • KulfanParameters is 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's live_shape field, which cost a refresh 48 bytes a panel even when the shape had not changed; a panel's live_shape is now the very object it was sampled from rather than a copy that compares equal to it. Two separately built KulfanParameters therefore no longer compare === or == on identical contents.

VortexStepMethod v4.3.0 2026-08-31

Fixed

  • The NeuralFoil surface pressure is now reconstructed from the predicted edge velocities in arc length, not by interpolating Cp in chord fraction. NeuralFoil samples at the cell centres of a uniform grid, so it reports nothing over the first and last 1/2N of chord; the old code extrapolated Cp off the end of each surface independently. Through a stagnation point ue is linear in arc length while Cp is quadratic, so that extrapolated a parabola through its own turning point: on an inflated section it put Cp = -1.60 at 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_section returns ue_upper/ue_lower alongside the pressures.

Added

  • 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 a POLAR_MATRICES wing takes one too, at every delta it 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: KulfanBasis and deform_kulfan deform a fixed Kulfan fit analytically (a matvec against a constant CST basis, never a refit, which is non-unique); control_point_deflection resamples a deflection given at arbitrary chord fractions — beam nodes, membrane nodes — onto that basis. chord_residual first takes the straight line through the deflection's own endpoints off it, which the basis cannot express and answers with runaway weights; chord_line returns that line, since its rotation is one the caller may still owe its angle of attack. panel_kulfan_parameters fits 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. LivePolars and refresh_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 — LivePolarSettings carries the offsets off its current angle of attack — and polar_drift reports 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's Cp from its deformed shape in one batched forward pass at the converged angle of attack, resampled onto the panel's own contour nodes by contour_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_matrix and live_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_velocity and contour_pressure also back the NeuralFoilSolver contour assembly, so the live path and the offline table generator reconstruct pressure the same way.
  • compare_live_polar solves 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 under live_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 as alpha beside the requested one. A LivePolars now 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_inputs accepts 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 tabulated delta contour 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. KulfanParameters moved from AirfoilAero into the core module for it; it is still exported from both.

Changed

  • A POLAR_VECTORS table 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.

VortexStepMethod v4.2.0 2026-08-25

Added

  • 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_loads and the small helpers around them. update_panel_properties!, init_pos!, calc_forces! and calculate_results now call them instead of spelling the algebra out three times, and SymbolicAWEModels traces 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_alpha and the deficiency argument of panel_inflow carry 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.

Changed

  • section_pitch_rate gained 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 is 1e-12 m, which moves no reported quantity.

VortexStepMethod v4.1.2 2026-08-22

Fixed

  • Billowing now takes its rotation axis from the section pair itself instead of projecting the leading-edge span vector on spanwise_direction. Sections run +y to -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.

VortexStepMethod v4.1.1 2026-08-19

Fixed

  • 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 as theta^2.

VortexStepMethod v4.1.0 2026-08-17

Added

  • table_format keyword on write_section_aero, generate_airfoils, obj_to_yaml and surfplan_to_aero_yaml: :csv (default, readable) or :arrow (binary, ~40× faster to load and 2.6× smaller). read_section_aero detects the format from the file suffix, so a geometry YAML can reference either.
  • geometry_path keyword on obj_to_yaml, naming the geometry YAML itself instead of always writing output_dir/geometry.yaml. Point it outside the table directory and the emitted table references carry the path from the YAML's directory to output_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_table and write_node_rows rewrite a per-node table in the format the destination suffix names. obj_to_yaml migrates an existing dataset with them when table_format differs from what the directory holds, so a dataset changes format without re-running the airfoil solver that produced it.
  • flow_curvature solver setting (default false). When enabled, each section gets the thin-airfoil pitch-rate moment increment Δcm = -(π/4) q̂ with q̂ = 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 to q was already exact because the inflow is sampled at the three-quarter-chord point, so only the moment was missing.
  • pitch_rate_dist field on BodyAerodynamics, holding the q above per panel. set_va!(body_aero, va, omega) fills it by projecting omega onto every panel's y_airf, so panels at different dihedral see different rates from one body rate. The distributed set_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 stale omega, 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_airf for rigid motion.
  • bin/update_default_manifests regenerates both checked-in default manifests in one command, and bin/install takes +X.Y / --version X.Y to 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.

Fixed

  • The Solver docstring quoted the SolverSettings defaults for type_initial_gamma_distribution and core_radius_fraction (ELLIPTIC, 1e-20) instead of its own (ZEROS, 0.05), and never said what core_radius_fraction measures. It now documents the Solver defaults and cites Damiani et al. (2019) for the 0.05 cut-off.
  • A remesh under use_prior_polar no longer resamples the refined sections' SectionAero surface tables down to whatever unrefined sections survive it. compute_refined_section_interpolation! reblended contour, cp and cf from the unrefined sections unconditionally while aero_data was 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 overwrites body_aero.AIC with the aerodynamic-centre (LLT) matrix. It builds that matrix in its own AIC_aero_center field, so AIC keeps holding the control-point matrix the circulation was solved against, which is what anything reading it after a solve expects. Costs a second n_panels × n_panels × 3 buffer.
  • obj_to_yaml no longer places sections on a wingtip that has closed to a point. station_indices spreads 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 default wingtip_distance used to put a zero-chord section at each tip, whose polar was NaN and took the whole solve with it; working around it meant guessing a wingtip_distance large enough to skip past the tip. That workaround is no longer the default: wingtip_distance is now 0.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 +y to -y order on load (normalize_span_order!), and by refine! for wings built through add_section!. Panel y_airf and z_airf follow 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 crossed spanwise_direction. obj_to_yaml and surfplan_to_aero_yaml emit that order too; files of either order keep loading the same.
  • Spanwise distribution plots put +y on the left, matching that order and the kite seen from the front.
  • The lofted airfoil skin in plot_geometry draws 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 -y to +y got every rib drawn at its neighbour's station, one tip bare and the other doubled, while the panels themselves rendered correctly.

Changed

  • SolverSettings now defaults to the same values as Solver: core_radius_fraction 1e-20 → 0.05 and type_initial_gamma_distribution ELLIPTIC → 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 upstream awegroup/Vortex-Step-Method default; at 1e-20 the 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 in data/ sets both keys explicitly.
  • BodyAerodynamics.AIC is stored as (n_panels, n_panels, 3) instead of (3, n_panels, n_panels), so each component slice AIC[:, :, k] is contiguous and the induced-velocity products reach BLAS gemv instead 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 reading AIC[k, i, j] must become AIC[i, j, k].
  • The filament induced-velocity kernels reuse the r0/length each BoundFilament already 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_table parses into a preallocated matrix instead of reduce(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=true draws 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_plot takes the window name as a keyword.
  • examples/V3_kite.jl builds its wing from VSMSettings and adds a second sweep on polars generated from V3_25.obj with NeuralFoil (NEURALFOIL, on by default), so the plots compare CAD-derived polars against the checked-in CFD tables.
  • bin/run_julia starts Julia with JULIA_NUM_THREADS=auto unless the environment already sets it.

VortexStepMethod v4.0.0 2026-08-03

Added

  • NeuralFoil-based airfoil polar generation via the new AirfoilAero submodule (NeuralFoilSolver, XFoilSolver, analyze_section, analyze_sweep, fit_kulfan_parameters, shrink_wrap, ShrinkWrap)
  • ObjAdapter submodule: converts a 3D wing .obj mesh to the native YAML/CSV geometry format (obj_to_yaml, perpendicular_sections, write_yaml, plot_slices_3d, plot_airfoils)
  • SurfplanAdapter submodule: surfplan_to_aero_yaml turns a SurfplanAdapter aero export into the native pressure-ready YAML/CSV geometry (shared generate_airfoils core with ObjAdapter)
  • ObjWing(obj_path[, dat_path]; Re, n_panels, aero_solver, remake, ...) convenience constructor restored for backward compatibility — internally calls ObjAdapter.obj_to_yaml then Wing; aero_solver selects the polar backend (default NeuralFoilSolver(); pass XFoilSolver() for old behavior); remake=false (default) reuses an existing geometry.yaml in output_dir to skip expensive polar generation when only n_panels changes; set remake=true to force regeneration
  • Per-section surface aero tables: SectionAero (full contour + surface pressure cp + skin friction cf per node over an (α, δ) grid), section_surface, read_section_aero / write_section_aero; surface aero propagates through Section and Wing and is spanwise-interpolated during refine!
  • POLY aero model for polynomial cl/cd/cm (exported)
  • lei_poly_coeffs(tube_diameter, camber) (exported from AirfoilAero) 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 (default false) and artificial_viscosity_factor (default 0.035) on the solver settings
  • crease_frac wing setting (chordwise flap-hinge fraction, default 0.75) for drawing the δ-deflected plate/skin, readable from YAML
  • examples/V3_neuralfoil.jl and examples/obj_to_yaml_kite.jl

Changed

  • BREAKING - ObjWing polar generation now uses NeuralFoil (via ObjAdapter.obj_to_yaml) instead of XFoil + a user-supplied .dat file. The constructor signature is backward-compatible (dat_path is accepted and silently ignored), but the resulting aerodynamic polars will differ numerically from v3.x outputs. To reproduce old XFoil-based polars pass aero_solver=XFoilSolver() to ObjAdapter.obj_to_yaml and call Wing directly.
  • OBJ-based wings are now built via ObjAdapter.obj_to_yaml + Wing(yaml_path) instead of the old ObjWing pipeline (XFoil + single .dat file); ObjWing is kept as a shim that accepts but ignores dat_path
  • BREAKING - LEI_AIRFOIL_BREUKELS is now a deprecated alias of POLY, 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 with lei_poly_coeffs(tube_diameter, camber)
  • Section and add_section! accept an optional section_aero argument
  • BREAKING - plotting is now Makie-only; the VortexStepMethodMakieExt extension loads once a Makie backend and MakieControlPlots are available, so plotting code must now load MakieControlPlots (and drop set_plot_backend!); plot_section_polars is rendered through MakieControlPlots

Removed

  • ObjWing as a standalone pipeline (replaced by ObjAdapter); the name is re-exported as a compatibility wrapper
  • auto_rotation helper (internal, removed from public API)
  • PanelGroupingMethod enum (already removed in v3.0.0 — stale docs entry cleaned up)
  • ControlPlots test run removed from CI (plot-controlplots arg) due to a libraqm/HarfBuzz symbol conflict in the GitHub Actions environment
  • BREAKING - the ControlPlots plotting extension, examples_cp/, and the PythonCall/Matplotlib setup (bin/install_controlplots, CondaPkg LocalPreferences defaults)
  • BREAKING - the PlotBackend/MakieBackend/ControlPlotsBackend types and set_plot_backend!; plotting works as soon as a Makie backend and MakieControlPlots are loaded
  • the never-implemented plot_circulation_distribution

VortexStepMethod v3.3.6 2026-06-13

Added

  • calc_forces! and solve_base! (both exported): solve! is now solve_base! followed by calc_forces!, so a frozen circulation can be mapped to forces without re-running the nonlinear gamma solve (#245)
  • calculate_cd and calculate_cm, splitting the combined calculate_cd_cm into separate drag- and moment-coefficient functions; calculate_cd_cm is kept as a thin wrapper (#246)

Changed

  • calc_forces! is now allocation-free in the per-step hot path (preallocated panel_area_dist and unrefined_count_dist buffers) (#245)

Fixed

  • 3D polar plotting (#245)
  • flaky Aqua persistent_tasks test now actually disabled via persistent_tasks=false (() did not disable it) (#246)

VortexStepMethod v3.3.5 2026-06-05

Added

  • moment_coeff_unrefined_dist field in VSMSolution: the summed moment_frac-referenced pitching-moment coefficient per unrefined section [-]

VortexStepMethod v3.3.4 2026-05-31

Added

  • PlotBackend, MakieBackend, ControlPlotsBackend, and set_plot_backend! so applications can explicitly choose which plotting extension the backend-agnostic plotting API should use
  • PythonCall added as a weak dependency to support the ControlPlots backend with PythonPlot

Changed

  • 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 ControlPlots compatibility to include both 0.2.5 and 0.3
  • improved bin/install and bin/install_controlplots scripts

Fixed

  • 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 ControlPlotsExt for PythonPlot compatibility (plot_distribution no longer errors with PythonPlot backend)
  • fixed missing initialization of damp in solver.jl

VortexStepMethod v3.3.3 2026-05-21

Fixed

  • MakieExt and ControlPlotsExt no longer both define VortexStepMethod.plot_geometry for the same type, resolving a method ambiguity when both extensions were loaded (#236)
  • menu() and menu_cp() now always set the active backend before dispatching to a plot function, preventing stale-backend errors

VortexStepMethod v3.3.2 2026-05-18

Changed

  • use 2-arg version of atan to avoid possible NaN

VortexStepMethod v3.3.1 2026-05-13

Changed

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

Fixed

  • smooth_sqrt in the solver hot loop keeps gradients defined at zero velocity magnitude

VortexStepMethod v3.3.0 2026-05-05

Added

  • ForwardDiff compatibility, used by default in linearize (#232)
  • backend keyword argument for linearize
  • example linearize_check.jl comparing FiniteDiff and ForwardDiff tangents

Changed

  • core structs are parameterized on the scalar type T so dual numbers can propagate through them; public constructors are unchanged

VortexStepMethod v3.2.0 2026-05-02

Added

  • support for both CairoMakie and GLMakie in the example menu() via new CairoMakie.activate() / GLMakie.activate() entries (#222)
  • plots are saved as PDF when CairoMakie is active
  • bin/install now creates the output folder used by examples to store generated plots

Changed

  • examples write plots into the shared output folder instead of the working directory
  • example plot file names are sanitized: spaces replaced with _ and % replaced with pct

Fixed

  • NONLIN solver no longer returns stale gamma on repeated solve! calls (#228)

VortexStepMethod v3.1.3 2026-04-23

Fixed

  • bug in linearize where body_aero.va was used instead of body_aero._va, causing incorrect initial velocity storage (#227)
  • set_va! no longer overwrites _va with a computed reference velocity when omega is nonzero; this simplifies the function and fixes linearization with turn rates
  • linearize now correctly preserves and restores omega across perturbations

VortexStepMethod v3.1.2 2025-04-20

Added

  • add back compat entry v2 for SciMLBase

VortexStepMethod v3.1.1 2025-04-20

Added

  • add back compat entry v3 for RecursiveArrayTools

VortexStepMethod v3.1.0 2025-04-19

Breaking

  • billowing_angle replaced by billowing_percentage on Wing and WingSettings (percentage of arc length, not radians)
  • billowing_angle_from_percentage() removed
  • BILLOWING distribution now uses sinusoidal rotation instead of circular arc

Added

  • billowing.jl example comparing flat vs billowed V3 kite
  • Coarse V3 kite geometry, settings, and combined CFD polar data
  • cl_over_cd keyword for plot_polars and plot_combined_analysis
  • the function menu_cp() can now be used to run the examples with the ControlPlots backend
  • the script bin/install can and should be used to instantiate the project and all sub-projects after git checkout
  • the files Manifest-v1.11.toml.default and Manifest-v12.toml.default for enhanced reproducibility
  • the scripts bin/jetls and bin/jetls_examples for running a static check of the source code
  • the script test_bench.jl for benchmarking the refinement method and measuring allocations

Changed

  • 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

Fixed errors

  • Domain error in elliptical gamma distribution when control points lie outside the nominal span envelope

VortexStepMethod v3.0.1 2025-04-04

Changed

  • the file CITATION.cff
  • compat entry for RecursiveArrayTools

Added

  • the file .zenodo.json

VortexStepMethod v3.0.0

Breaking Changes

Unified Wing type

  • Wing and RamAirWing merged into a single Wing struct
  • RamAirWing constructor replaced by ObjWing (returns a Wing with OBJ-specific fields populated)
  • Renamed ram_geometry.jl → obj_geometry.jl

Unrefined sections replace panel grouping

  • n_groups replaced by n_unrefined_sections in Wing, WingSettings, and SolverSettings
  • PanelGroupingMethod enum removed
  • Each refined panel maps back to its unrefined section via refined_panel_mapping
  • n_groups and grouping_method in YAML settings files emit a deprecation warning and are ignored

Renamed VSMSolution fields (_array → _dist)

  • panel_width_array → width_dist
  • alpha_array → alpha_dist
  • cl_array → cl_dist, cd_array → cd_dist, cm_array → cm_dist
  • panel_lift → lift_dist, panel_drag → drag_dist
  • panel_moment → panel_moment_dist
  • group_moment_dist → moment_unrefined_dist
  • Type parameter Solver{P,G} / VSMSolution{P,G} changed to Solver{P,U} / VSMSolution{P,U} (U = unrefined sections)

Mesh refinement separated from reinit

  • reinit! no longer re-refines the mesh; it only updates panel properties from existing refined_sections
  • New refine! function handles mesh refinement explicitly

Changed

  • Consistent naming convention: _dist = per-panel, _unrefined_dist = per-unrefined-section
  • correct_aoa solver setting added (default false)
  • alpha_geometric is now the angle between apparent wind and chord
  • Panel width calculation uses sum instead of average
  • src/plotting.jl removed; all plotting moved to Makie and ControlPlots extensions
  • TestEnv no longer required as a global dependency

Added

  • n_unrefined_sections field in Wing for tracking pre-refinement sections
  • refined_panel_mapping for 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_dist field in VSMSolution
  • unrefined_deform! for applying twist and TE deflection to unrefined sections; non_deformed_sections preserved for reset/re-deformation
  • refine! function for explicit mesh refinement
  • plot_combined_analysis in the Makie extension
  • Literature comparison plotting in the Makie extension
  • obj_file and dat_file fields in WingSettings
  • sort_sections kwarg for section ordering control
  • Separate test/Project.toml and examples/Project.toml
  • New tests: test_unrefined_dist.jl, test_refinement_validation.jl, test_yaml_wing_deformation.jl

Removed

  • RamAirWing type (use ObjWing or Wing directly)
  • PanelGroupingMethod enum
  • n_groups from WingSettings, SolverSettings, and YAML files
  • src/plotting.jl (920 lines moved to extensions)

VortexStepMethod v2.3.0 2025-10-16

Added

  • A Makie plotting extension.

VortexStepMethod v2.2.1 2025-10-07

Fixed

  • Reference in vsm_settings.yaml now points to aero_geometry.yaml.

Added

  • None.

Removed

  • Bridle configuration data (nodes, lines, connections).

Changed

  • Renamed wing_geometry_polars_CFD.yaml → aero_geometry.yaml.

VortexStepMethod v2.2.0 2025-08-28

Added

  • The kwarg aero_coeffs to the function linearize: if true the linearization will output normalized coefficients instead of moments and forces.

VortexStepMethod v2.1.0 2025-08-11

Changed

1. Core New Functionality: YAML Geometry Support

  • New file: yaml_geometry.jl (290+ lines) — Complete YAML-based wing geometry loading.
  • New structs: WingSectionData, WingAirfoilData, WingAirfoilInfo (with @with_kw macros).
  • 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.

2. Enhanced Settings System

  • Renamed vs() to VSMSettings() constructor.
  • Added convenience Solver(body_aero, settings) constructor.
  • Improved settings structure and validation.

3. Comprehensive Test Infrastructure

  • Split tests: Reorganized from monolithic files to modular structure (test/module_name/test_*.jl).
  • New test module: yaml_geometry with test_load_polar_data.jl and test_wing_constructor.jl.
  • Test utilities: test_data_utils.jl with shared helper functions.
  • Test data: Extensive YAML and CSV test files in data.

4. Data and Examples

  • New data sets: Complete TUDELFT_V3_KITE with 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.

5. Core Module Improvements

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

VortexStepMethod v2.0.0 2025-07-11

Changed

  • bump Interpolations to 0.16
  • breaking: rename init! to reinit!

VortexStepMethod v1.2.6 2025-04-30

Changed

  • update NonlinearSolve.jl

VortexStepMethod v1.2.5 2025-04-18

Changed

  • suppress @info messages when creating a RamAirWing
  • improve examples ram_air_kite.jl and bench.jl

VortexStepMethod v1.2.4 2025-04-17

Changed

  • implement export of solve and solve! correctly
  • do not export menu() because KiteUtils exports it
  • the polars of the ram air kite are now saved in .csv files
  • update CI.yml

VortexStepMethod v1.2.3 2025-04-13

Changed

  • expose the angle of attack alpha_array in the VSMSolution #167

VortexStepMethod v1.2.2 2025-04-13

Added

  • the parameter prnto the constructor RamAirWing which allows it suppress info messages

VortexStepMethod v1.2.1 2025-04-08

Added

  • Add back bench2.jl and rename it to bench_solve.jl #150

Removed

  • Remove problematic parallel Xfoil computing and use single thread instead #161

VortexStepMethod v1.2.0 2025-03-27

Changed

  • The fields that had as type a Matrix of size Px1 have now the type Vector
  • Many new fields of the type VSMSolution documented
  • reinit!(body_aero) is now a public function

Added

  • Added the option to use nonlinear solve to calculate the gamma distribution #140
  • New page Tips and tricks added to the documentation
  • Fast and modular linearization added around an operating point #140

VortexStepMethod v1.1.2 2025-03-23

Added

  • The function install_examples() which allows to easily install the examples without using git
  • The function solve! returns a struct now. The function solvethat returns a dict is still available.
  • The moment coefficients distribution in solve!
  • The script install to the bin folder for users who checked out this git repository
  • The script bench2.jl was added for allocation testing of the solve! function

Changed

  • Read the y-coordinates in the correct direction from the ram_air_kite_body.obj file
  • In the menu.jl, changed help to help_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 RamAirWing is removed, and the geometry is created such that [0, 0, 0] is the center of mass
  • The enumeration WingType was 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_principal option added to RamAirWing
  • deform! by a distribution instead of just a left and right angle

Fixed

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

VortexStepMethod v1.1.0 2025-03-04

Added

  • 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 a VSMSolution struct #87
  • Add the option to remove the NaNs in aero_data vectors or matrices using the remove_nan keyword in the Wing and RamAirWing constructors #98

Changed

  • Add origin argument to BodyAerodynamics constructor #66
  • Improve documentation

Initial Release

This project is based on version 1.0 of the Python project Vortex-Step-Method

Noteworthy Differences of v1.0.1 to the Python version

  • implemented in Julia, therefore about 50 times faster
  • an importer for .obj wing geometry files was added (see: #10)
  • automatic creation of polars using Xfoil.jl was added (see: #43)
  • a ram-air kite example was added
  • Umag was replaced with v_a as 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