Skip to content

Commit ad9d635

Browse files
committed
Update billowing to use catenary
1 parent 270f5bd commit ad9d635

10 files changed

Lines changed: 226 additions & 114 deletions

File tree

NEWS.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
## Unreleased
22

3+
### Breaking
4+
- `billowing_angle` replaced by `billowing_percentage` on `Wing` and
5+
`WingSettings` (percentage of arc length, not radians)
6+
- `billowing_angle_from_percentage()` removed
7+
- `BILLOWING` distribution now uses catenary curve instead of circular arc
8+
39
### Added
4-
- `BILLOWING` panel distribution with circular arc TE billowing between ribs
5-
- `billowing_angle` field on `Wing` and `WingSettings`
6-
- `billowing_angle_from_percentage()` utility function
710
- `billowing.jl` example comparing flat vs billowed V3 kite
811
- Coarse V3 kite geometry, settings, and combined CFD polar data
912
- `cl_over_cd` keyword for `plot_polars` and `plot_combined_analysis`

docs/src/functions.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ refine!
88
calculate_span
99
calculate_projected_area
1010
load_polar_data
11-
billowing_angle_from_percentage
1211
```
1312

1413
## Setting the inflow conditions and solving

examples/billowing.jl

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,11 @@ solver_cfg = settings_data["solver_settings"]
2828
wing_cfg = settings_data["wings"][1]
2929
n_panels = wing_cfg["n_panels"]
3030

31-
# Read billowing angle from settings (degrees → radians, or from percentage)
32-
if haskey(wing_cfg, "billowing_percentage")
33-
BILLOWING_ANGLE = billowing_angle_from_percentage(
34-
wing_cfg["billowing_percentage"])
35-
else
36-
BILLOWING_ANGLE = deg2rad(get(wing_cfg, "billowing_angle", 0.0))
37-
end
31+
BILLOWING_PCT = get(wing_cfg, "billowing_percentage", 0.0)
3832

3933
labels = [
4034
"VSM flat",
41-
"VSM billowing $(round(Int, rad2deg(BILLOWING_ANGLE)))°",
35+
"VSM billowing $(BILLOWING_PCT)%",
4236
"CFD Re=5e5",
4337
"CFD Re=10e5",
4438
"VSM Python Re=5e5",
@@ -51,10 +45,11 @@ geom_data = VortexStepMethod.YAML.load_file(
5145
section_headers = geom_data["wing_sections"]["headers"]
5246
section_rows = geom_data["wing_sections"]["data"]
5347

54-
function build_wing(; distribution=SPLIT_PROVIDED, billowing_angle=0.0)
48+
function build_wing(; distribution=SPLIT_PROVIDED,
49+
billowing_percentage=0.0)
5550
wing = Wing(n_panels;
5651
spanwise_distribution=distribution,
57-
billowing_angle=billowing_angle)
52+
billowing_percentage=billowing_percentage)
5853
for row in section_rows
5954
d = Dict(zip(section_headers, row))
6055
le = [d["LE_x"], d["LE_y"], d["LE_z"]]
@@ -74,7 +69,7 @@ VortexStepMethod.reinit!(body_aero_flat)
7469

7570
# --- Wing with billowing ---
7671
wing_bill = build_wing(distribution=BILLOWING,
77-
billowing_angle=BILLOWING_ANGLE)
72+
billowing_percentage=BILLOWING_PCT)
7873
body_aero_bill = BodyAerodynamics([wing_bill])
7974
VortexStepMethod.reinit!(body_aero_bill)
8075

@@ -143,8 +138,7 @@ if PLOT
143138
angle_of_attack=angle_of_attack_deg,
144139
side_slip=sideslip_deg,
145140
v_a=wind_speed,
146-
title="V3 Kite: flat vs billowing " *
147-
"$(round(Int, rad2deg(BILLOWING_ANGLE)))°",
141+
title="V3 Kite: flat vs billowing $(BILLOWING_PCT)%",
148142
is_show=false,
149143
use_tex=USE_TEX,
150144
angle_of_attack_for_spanwise_distribution=10.0,

src/VortexStepMethod.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export BodyAerodynamics
3232
export Solver, solve, solve_base!, solve!, VSMSolution, linearize
3333
export calculate_results
3434
export add_section!, set_va!
35-
export calculate_span, calculate_projected_area, billowing_angle_from_percentage
35+
export calculate_span, calculate_projected_area
3636
export MVec3
3737
export Model, VSM, LLT
3838
export AeroModel, LEI_AIRFOIL_BREUKELS, POLAR_VECTORS, POLAR_MATRICES, INVISCID
@@ -130,14 +130,14 @@ Enumeration of the implemented panel distributions.
130130
- COSINE # Cosine distribution
131131
- `SPLIT_PROVIDED` # Split provided sections
132132
- `UNCHANGED` # 1:1 copy of unrefined to refined sections (no interpolation)
133-
- `BILLOWING` # Linear distribution with circular arc TE billowing
133+
- `BILLOWING` # Split provided + catenary TE billowing between ribs
134134
"""
135135
@enum PanelDistribution begin
136136
LINEAR # Linear distribution
137137
COSINE # Cosine distribution
138138
SPLIT_PROVIDED # Split provided sections
139139
UNCHANGED # 1:1 copy of unrefined to refined sections
140-
BILLOWING # Linear + circular arc TE billowing
140+
BILLOWING # Split provided + catenary TE billowing
141141
end
142142

143143
"""

src/obj_geometry.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ function ObjWing(
580580
cache = [PreallocationTools.LazyBufferCache()]
581581

582582
wing = Wing(n_panels, Int16(n_unrefined_sections), spanwise_distribution, panel_props, MVec3(spanwise_direction),
583-
sections, Section[], remove_nan, use_prior_polar, 0.0, # billowing_angle
583+
sections, Section[], remove_nan, use_prior_polar, 0.0, # billowing_percentage
584584
Int16[], # refined_panel_mapping empty
585585
Section[], zeros(n_panels), zeros(n_panels), # non_deformed, theta, delta
586586
mass, gamma_tip, inertia_tensor, T_cad_body, R_cad_body, radius,

src/settings.jl

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,8 @@ Settings for a single wing, used within [`VSMSettings`](@ref).
3434
(default `true`)
3535
- `use_prior_polar`: Reuse prior refined/panel polar mapping on
3636
reinit/refine updates (default `false`)
37-
- `billowing_angle`: Half-angle of circular arc billowing in degrees
37+
- `billowing_percentage`: TE billow as percentage of arc length
3838
(default `0.0`; only used with `BILLOWING` distribution).
39-
Converted to radians internally.
40-
Mutually exclusive with `billowing_percentage`.
41-
- `billowing_percentage`: Percentage by which the chord is shorter than
42-
the minor arc (default `nothing`; converted to `billowing_angle`).
43-
Mutually exclusive with `billowing_angle`.
4439
"""
4540
@with_kw mutable struct WingSettings
4641
name::String = "main_wing"
@@ -52,7 +47,7 @@ Settings for a single wing, used within [`VSMSettings`](@ref).
5247
spanwise_direction::MVec3 = [0.0, 1.0, 0.0]
5348
remove_nan = true
5449
use_prior_polar::Bool = false
55-
billowing_angle::Float64 = 0.0 # half-angle of billowing arc [rad]
50+
billowing_percentage::Float64 = 0.0 # TE billow as % of arc length
5651
end
5752

5853
"""
@@ -179,19 +174,9 @@ function VSMSettings(filename; data_prefix=true)
179174
wing.remove_nan = wing_data["remove_nan"]
180175
wing.use_prior_polar = get(wing_data, "use_prior_polar", false)
181176

182-
has_angle = haskey(wing_data, "billowing_angle")
183-
has_pct = haskey(wing_data, "billowing_percentage")
184-
if has_angle && has_pct
185-
throw(ArgumentError(
186-
"Wing '$(wing.name)': specify either " *
187-
"billowing_angle or billowing_percentage, " *
188-
"not both"))
189-
elseif has_pct
190-
wing.billowing_angle =
191-
billowing_angle_from_percentage(
192-
wing_data["billowing_percentage"])
193-
elseif has_angle
194-
wing.billowing_angle = deg2rad(wing_data["billowing_angle"])
177+
if haskey(wing_data, "billowing_percentage")
178+
wing.billowing_percentage =
179+
Float64(wing_data["billowing_percentage"])
195180
end
196181

197182
push!(vsm_settings.wings, wing)

src/wing_geometry.jl

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ mutable struct Wing <: AbstractWing
232232
refined_sections::Vector{Section}
233233
remove_nan::Bool
234234
use_prior_polar::Bool
235-
billowing_angle::Float64 # Half-angle of circular arc [rad] (0=straight, π/2=semicircle)
235+
billowing_percentage::Float64 # TE billow as percentage of arc length (0=flat)
236236

237237
# Grouping
238238
refined_panel_mapping::Vector{Int16} # Maps each refined panel index to unrefined section index (1 to n_unrefined_sections)
@@ -262,7 +262,7 @@ end
262262
spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]),
263263
remove_nan::Bool=true,
264264
use_prior_polar::Bool=false,
265-
billowing_angle::Float64=0.0)
265+
billowing_percentage::Float64=0.0)
266266
267267
Constructor for a [Wing](@ref) struct with default values that initializes the sections
268268
and refined sections as empty arrays. Creates a basic wing suitable for YAML-based construction.
@@ -274,15 +274,15 @@ and refined sections as empty arrays. Creates a basic wing suitable for YAML-bas
274274
- `spanwise_direction::MVec3` = MVec3([0.0, 1.0, 0.0]): Wing span direction vector
275275
- `remove_nan::Bool`: Whether to remove the NaNs from interpolations or not
276276
- `use_prior_polar::Bool`: Reuse prior refined/panel polar mapping during geometry-only updates
277-
- `billowing_angle::Float64`: Half-angle of billowing arc in radians (0=straight, π/2=semicircle)
277+
- `billowing_percentage::Float64`: TE billow as percentage of arc length (0=flat)
278278
"""
279279
function Wing(n_panels::Int;
280280
n_unrefined_sections=nothing,
281281
spanwise_distribution::PanelDistribution=LINEAR,
282282
spanwise_direction::PosVector=MVec3([0.0, 1.0, 0.0]),
283283
remove_nan=true,
284284
use_prior_polar=false,
285-
billowing_angle=0.0)
285+
billowing_percentage=0.0)
286286

287287
# For YAML wings, n_unrefined_sections will be set when sections are added
288288
# Set to 0 as placeholder for now
@@ -293,7 +293,7 @@ function Wing(n_panels::Int;
293293
# Initialize with default/empty values for optional fields
294294
Wing(
295295
n_panels, n_unrefined_sections_value, spanwise_distribution, panel_props, spanwise_direction,
296-
Section[], Section[], remove_nan, use_prior_polar, Float64(billowing_angle),
296+
Section[], Section[], remove_nan, use_prior_polar, Float64(billowing_percentage),
297297
# Grouping
298298
Int16[],
299299
# Deformation fields
@@ -1115,17 +1115,18 @@ function refine_mesh_for_linear_cosine_distribution!(
11151115
end
11161116

11171117
"""
1118-
refine_mesh_by_splitting_provided_sections!(wing; reuse_aero_data, billowing_angle)
1118+
refine_mesh_by_splitting_provided_sections!(wing; reuse_aero_data,
1119+
billowing_percentage)
11191120
11201121
Refine mesh by splitting provided sections into desired number of panels.
11211122
1122-
When `billowing_angle > 0`, applies circular arc TE displacement to intermediate
1123+
When `billowing_percentage > 0`, applies catenary TE displacement to intermediate
11231124
sections within each rib pair (simulating fabric billowing between ribs).
11241125
"""
11251126
function refine_mesh_by_splitting_provided_sections!(
11261127
wing::AbstractWing;
11271128
reuse_aero_data::Bool=false,
1128-
billowing_angle::Float64=0.0
1129+
billowing_percentage::Float64=0.0
11291130
)
11301131
n_sections_provided = length(wing.unrefined_sections)
11311132
n_panels_provided = n_sections_provided - 1
@@ -1212,8 +1213,8 @@ function refine_mesh_by_splitting_provided_sections!(
12121213
reuse_aero_data
12131214
)
12141215

1215-
# Apply billowing arc to the just-created sections
1216-
if billowing_angle > 0 && idx > start_idx
1216+
# Apply catenary billowing to the just-created sections
1217+
if billowing_percentage > 0 && idx > start_idx
12171218
LE_1 = LE[left_section_index]
12181219
TE_1 = TE[left_section_index]
12191220
LE_2 = LE[left_section_index + 1]
@@ -1228,40 +1229,25 @@ function refine_mesh_by_splitting_provided_sections!(
12281229
if z_norm > 1e-10
12291230
z_hat = z_hat / z_norm
12301231

1231-
TE_mid = (TE_1 + TE_2) / 2
12321232
d = abs(dot(TE_1 - TE_2, y_hat))
1233-
span_len = norm(LE_1 - LE_2)
1234-
chord_len_1 = norm(chord_1)
1235-
chord_len_2 = norm(chord_2)
12361233

12371234
if d > 1e-12
1238-
R = (d / 2) / sin(billowing_angle)
1239-
h = (d / 2) / tan(billowing_angle)
1235+
a = catenary_parameter(
1236+
billowing_percentage, d)
1237+
u = d / (2a)
1238+
span_len = norm(LE_1 - LE_2)
1239+
TE_mid = (TE_1 + TE_2) / 2
12401240

12411241
for si in start_idx:(idx - 1)
12421242
sec = wing.refined_sections[si]
12431243
t = dot(sec.LE_point - LE_2,
12441244
y_hat) / span_len
1245-
1246-
arc_y = -R * sin(
1247-
billowing_angle * (1 - 2t))
1248-
arc_z = -h + R * cos(
1249-
billowing_angle * (1 - 2t))
1250-
arc_TE = TE_mid +
1251-
arc_y * y_hat + arc_z * z_hat
1252-
1253-
chord_dir = arc_TE - sec.LE_point
1254-
chord_dir_len = norm(chord_dir)
1255-
if chord_dir_len > 1e-12
1256-
target_chord = (
1257-
t * chord_len_1 +
1258-
(1 - t) * chord_len_2)
1259-
sec.TE_point .= (
1260-
sec.LE_point +
1261-
(target_chord /
1262-
chord_dir_len) *
1263-
chord_dir)
1264-
end
1245+
x = d * (t - 0.5)
1246+
sag = a * (cosh(x / a) -
1247+
cosh(u))
1248+
arc_y = d * (t - 0.5)
1249+
sec.TE_point .= TE_mid +
1250+
arc_y * y_hat + sag * z_hat
12651251
end
12661252
end
12671253
end
@@ -1293,63 +1279,59 @@ end
12931279

12941280

12951281
"""
1296-
billowing_angle_from_percentage(percentage)
1282+
catenary_parameter(percentage, d)
12971283
1298-
Compute the billowing half-angle θ (in radians) such that the chord between two
1299-
circle intersections is `percentage`% shorter than the minor arc.
1284+
Compute the catenary parameter `a` such that a catenary spanning distance `d`
1285+
has an arc length that is `percentage`% longer than `d`.
13001286
1301-
Solves `sin(θ) = θ * (1 - percentage / 100)` using Newton's method.
1287+
Solves `u / sinh(u) = 1 - percentage / 100` where `u = d / (2a)`,
1288+
using Newton's method.
13021289
13031290
# Arguments
1304-
- `percentage::Real`: How much shorter the chord is relative to the arc (0–100).
1305-
0 means chord equals arc (θ → 0), ~36.3 means a semicircle (θ = π/2).
1291+
- `percentage::Real`: How much shorter the chord is relative to the arc
1292+
(0–100). 0 means no sag (flat), larger values mean deeper catenary.
1293+
- `d::Real`: Span distance between the two endpoints.
13061294
13071295
# Returns
1308-
- `Float64`: The half-angle θ in radians (same convention as `Wing.billowing_angle`).
1309-
1310-
# Example
1311-
```julia
1312-
julia> rad2deg(billowing_angle_from_percentage(10))
1313-
43.66…
1314-
```
1296+
- `Float64`: The catenary parameter `a`.
13151297
"""
1316-
function billowing_angle_from_percentage(percentage::Real)
1317-
percentage == 0 && return 0.0
1298+
function catenary_parameter(percentage::Real, d::Real)
1299+
percentage == 0 && return Inf
13181300
0 < percentage || throw(ArgumentError(
13191301
"percentage must be ≥ 0, got $percentage"))
1320-
percentage < 100 * (1 - 2 / π) || throw(ArgumentError(
1321-
"percentage must be < $(100 * (1 - 2/π)) (semicircle limit), " *
1322-
"got $percentage"))
1323-
factor = 1 - percentage / 100
1324-
# Initial guess from Taylor expansion: sin(θ) ≈ θ - θ³/6
1325-
θ = sqrt(6 * percentage / 100)
1302+
percentage < 100 || throw(ArgumentError(
1303+
"percentage must be < 100, got $percentage"))
1304+
target = 1 - percentage / 100 # u / sinh(u) = target
1305+
# Initial guess from Taylor: u/sinh(u) ≈ 1 - u²/6
1306+
u = sqrt(6 * (1 - target))
13261307
for _ in 1:100
1327-
f = sin(θ) - θ * factor
1328-
df = cos(θ) - factor
1308+
f = u / sinh(u) - target
1309+
# d/du [u/sinh(u)] = (sinh(u) - u*cosh(u)) / sinh(u)^2
1310+
sh = sinh(u)
1311+
df = (sh - u * cosh(u)) / sh^2
13291312
δ = f / df
1330-
θ -= δ
1313+
u -= δ
13311314
abs(δ) < 1e-12 && break
13321315
end
1333-
return θ
1316+
return d / (2u)
13341317
end
13351318

13361319
"""
13371320
refine_mesh_with_billowing!(wing; reuse_aero_data)
13381321
1339-
Refine wing mesh using SPLIT_PROVIDED spacing with circular arc billowing.
1340-
1341-
Between each pair of unrefined (rib) sections, the trailing edge follows a circular arc
1342-
that bulges in the direction perpendicular to the chord and span (simulating fabric
1343-
billowing between ribs on a ram-air kite). The leading edge stays linearly interpolated.
1322+
Refine wing mesh using SPLIT_PROVIDED spacing with catenary TE billowing.
13441323
1345-
The arc half-angle is `wing.billowing_angle` (0 = straight line, π/2 = semicircle).
1324+
Between each pair of unrefined (rib) sections, the trailing edge follows a
1325+
catenary curve that sags perpendicular to the chord and span (simulating fabric
1326+
billowing between ribs on a ram-air kite). The leading edge stays linearly
1327+
interpolated.
13461328
13471329
Delegates to [`refine_mesh_by_splitting_provided_sections!`](@ref).
13481330
"""
13491331
function refine_mesh_with_billowing!(wing; reuse_aero_data::Bool=false)
13501332
refine_mesh_by_splitting_provided_sections!(
13511333
wing; reuse_aero_data,
1352-
billowing_angle=wing.billowing_angle
1334+
billowing_percentage=wing.billowing_percentage
13531335
)
13541336
end
13551337

0 commit comments

Comments
 (0)