From ac8581048cf0466947a610ab2cdf192c8367f609 Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:12:18 +0200 Subject: [PATCH 1/3] Start ELLIPTIC circulation on each wing's own ellipse calculate_circulation_distribution_elliptical_wing loops over the wings with a running panel offset and measures each control point along its wing's spanwise_direction from that wing's mid-span, so a body with more than one wing no longer throws. spanwise_extent gives the (lo, hi) projection that calculate_span and the ellipse both take; the scratch field BodyAerodynamics.y it used to fill goes. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 3 ++ docs/src/private_functions.md | 1 + src/body_aerodynamics.jl | 40 ++++++++----------- src/wing_geometry.jl | 29 +++++++------- .../test_body_aerodynamics.jl | 29 ++++++++++++++ 5 files changed, 65 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f65c25f9..d8a1b81b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,9 @@ ### Fixed +- `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. - `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. diff --git a/docs/src/private_functions.md b/docs/src/private_functions.md index 43016830..78840da8 100644 --- a/docs/src/private_functions.md +++ b/docs/src/private_functions.md @@ -33,6 +33,7 @@ calculate_relative_alpha_and_relative_velocity update_effective_angle_of_attack! calculate_stall_angle_list wing_span_flip +spanwise_extent calculate_circulation_distribution_elliptical_wing _compute_reference_velocity_from_distribution smooth_circulation! diff --git a/src/body_aerodynamics.jl b/src/body_aerodynamics.jl index be9f9156..6df4f6a2 100644 --- a/src/body_aerodynamics.jl +++ b/src/body_aerodynamics.jl @@ -27,7 +27,6 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru influence coefficients, used only for the corrected angle of attack - `projected_area::Float64` = 1.0: The area projected onto the xy-plane of the kite body reference frame [m²] - `c_ref::Float64` = 1.0: Reference chord length (max panel chord) [m] -- `y::MVector{P, Float64}` = MVector{P,Float64}(zeros(P)) - `cache::Vector{PreallocationTools.LazyBufferCache{typeof(identity), typeof(identity)}}` = [LazyBufferCache() for _ in 1:15] """ @with_kw mutable struct BodyAerodynamics{P, W<:AbstractWing, T, PN<:Panel{T}} @@ -49,7 +48,6 @@ Main structure for calculating aerodynamic properties of bodies. Use the constru AIC_aero_center::Array{T, 3} = zeros(T, P, P, 3) projected_area::T = one(T) c_ref::T = one(T) - y::MVector{P, T} = zeros(MVector{P, T}) cache::Vector{PreallocationTools.LazyBufferCache{typeof(identity), typeof(identity)}} = [LazyBufferCache() for _ in 1:15] end @@ -496,30 +494,26 @@ Returns: nothing end """ - calculate_circulation_distribution_elliptical_wing(body_aero::BodyAerodynamics, gamma_0=1.0) + calculate_circulation_distribution_elliptical_wing(gamma_i, body_aero::BodyAerodynamics, + gamma_0=1.0) -Calculate circulation distribution for an elliptical wing. - -Returns: nothing +Write into `gamma_i` an elliptic circulation of peak `gamma_0` over each wing, its control +points measured along that wing's `spanwise_direction` from the wing's mid-span. """ -function calculate_circulation_distribution_elliptical_wing(gamma_i, body_aero::BodyAerodynamics, gamma_0=1.0) - length(body_aero.wings) == 1 || throw(ArgumentError("Multiple wings not yet implemented")) - - wing_span = body_aero.wings[1].span - @debug "Wing span: $wing_span" - - # Calculate y-coordinates of control points - y = body_aero.y - for (i, panel) in pairs(body_aero.panels) - y[i] = panel.control_point[2] +function calculate_circulation_distribution_elliptical_wing(gamma_i, + body_aero::BodyAerodynamics, gamma_0=1.0) + panel_offset = 0 + for wing in body_aero.wings + lo, hi = spanwise_extent(wing) + axis = normalize(wing.spanwise_direction) + for i in panel_offset .+ (1:wing.n_panels) + span_position = dot(body_aero.panels[i].control_point, axis) - (lo + hi) / 2 + # Clamped: a control point can lie outside the span of the unrefined sections + gamma_i[i] = gamma_0 * sqrt(max(0.0, 1 - (2span_position / (hi - lo))^2)) + end + panel_offset += wing.n_panels end - - # Calculate elliptical distribution (clamp to avoid sqrt of negative - # when control points lie outside the nominal span envelope) - gamma_i .= gamma_0 * sqrt.(max.(0.0, 1 .- (2 .* y ./ wing_span).^2)) - - @debug "Calculated circulation distribution: $gamma_i" - nothing + return nothing end """ diff --git a/src/wing_geometry.jl b/src/wing_geometry.jl index 7329dcc2..c6b3e05d 100644 --- a/src/wing_geometry.jl +++ b/src/wing_geometry.jl @@ -1685,24 +1685,25 @@ function refine_mesh_with_billowing!(wing; reuse_aero_data::Bool=false) end """ - calculate_span(wing::AbstractWing) + spanwise_extent(wing::AbstractWing) -Calculate wing span along spanwise direction. +Lowest and highest projection of the unrefined sections' LE and TE points on `wing`'s +`spanwise_direction`, as `(lo, hi)` [m]. +""" +function spanwise_extent(wing::AbstractWing) + axis = normalize(wing.spanwise_direction) + return extrema(dot(point, axis) for section in wing.unrefined_sections + for point in (section.LE_point, section.TE_point)) +end -Returns: - Float64: Wing span +""" + calculate_span(wing::AbstractWing) + +Wing span along `spanwise_direction` [m]. """ function calculate_span(wing::AbstractWing) - # Normalize spanwise direction - vector_axis = wing.spanwise_direction ./ norm(wing.spanwise_direction) - - # Get all points - all_points = reduce(vcat, [[section.LE_point, section.TE_point] - for section in wing.unrefined_sections]) - - # Project points and calculate span - projections = [dot(point, vector_axis) for point in all_points] - return maximum(projections) - minimum(projections) + lo, hi = spanwise_extent(wing) + return hi - lo end # Project point onto plane diff --git a/test/body_aerodynamics/test_body_aerodynamics.jl b/test/body_aerodynamics/test_body_aerodynamics.jl index 7024b410..dda610f0 100644 --- a/test/body_aerodynamics/test_body_aerodynamics.jl +++ b/test/body_aerodynamics/test_body_aerodynamics.jl @@ -609,6 +609,35 @@ end @test sol.force[3] > 2single.force[3] end + @testset "ELLIPTIC starts each wing on its own ellipse" begin + control_y = [panel.control_point[2] for panel in single_aero.panels] + ellipse = sqrt.(1 .- (2control_y ./ span) .^ 2) + fin = Wing(n_panels; spanwise_direction=[0.0, 0.0, 1.0]) + for z in section_y .+ 5.0 + add_section!(fin, [0.0, 0.0, z], [1.0, 0.0, z], INVISCID) + end + refine!(fin) + body_aero = BodyAerodynamics([wing_pair(section_y, n_panels, span + 1.0); fin]) + gamma = zeros(3n_panels) + VortexStepMethod.calculate_circulation_distribution_elliptical_wing(gamma, + body_aero) + + @test gamma ≈ repeat(ellipse, 3) + end + + @testset "ELLIPTIC and ZEROS converge to the same gamma" begin + wings = wing_pair(section_y, n_panels, span + 1.0) + _, from_zeros = solve_wings(wings) + gamma_zeros = copy(from_zeros.gamma_distribution) + body_aero = BodyAerodynamics(wings; va=[10.0, 0.0, 1.0]) + solver = Solver(2n_panels, 2length(section_y); + type_initial_gamma_distribution=ELLIPTIC) + sol = solve!(solver, body_aero) + + @test sol.solver_status == FEASIBLE + @test sol.gamma_distribution ≈ gamma_zeros rtol=1e-4 + end + n_wing_sections = length(section_y) first_sections = 1:n_wing_sections second_sections = n_wing_sections+1:2n_wing_sections From 8576dd4da20fca68f5d9b7fa47cda1eb92662c7e Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:44:09 +0200 Subject: [PATCH 2/3] Pin a lone off-centre wing's ellipse and mark the removed y field BREAKING Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 ++ test/body_aerodynamics/test_body_aerodynamics.jl | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a1b81b..d25a50b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ - 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 `BodyAerodynamics` field `y` is removed; it was scratch space for the + `ELLIPTIC` initial circulation and nothing else read it. - 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` diff --git a/test/body_aerodynamics/test_body_aerodynamics.jl b/test/body_aerodynamics/test_body_aerodynamics.jl index dda610f0..4366b2d6 100644 --- a/test/body_aerodynamics/test_body_aerodynamics.jl +++ b/test/body_aerodynamics/test_body_aerodynamics.jl @@ -623,6 +623,11 @@ end body_aero) @test gamma ≈ repeat(ellipse, 3) + + lone_wing_gamma = zeros(n_panels) + VortexStepMethod.calculate_circulation_distribution_elliptical_wing( + lone_wing_gamma, BodyAerodynamics([inviscid_wing(section_y .+ 5.0; n_panels)])) + @test lone_wing_gamma ≈ ellipse end @testset "ELLIPTIC and ZEROS converge to the same gamma" begin From 83b5ef6d2fc722e6ecbffa36934c6f0b82b31c36 Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:36:57 +0200 Subject: [PATCH 3/3] Drop the changelog entry for the private BodyAerodynamics.y field Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a40e0189..6cab7d10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,8 +32,6 @@ - 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 `BodyAerodynamics` field `y` is removed; it was scratch space for the - `ELLIPTIC` initial circulation and nothing else read it. - BREAKING: the apparent wind is `va` for the speed [m/s], `va_vec` for the 3-vector and `va_dist` / `va_vec_dist` per panel, and the old names error: - `body_aero.va` becomes `body_aero.va_vec`, and `va=` becomes `va_vec=` in