Skip to content

Commit 9af0690

Browse files
1-Bort-1claude
andcommitted
Spread sliced sections evenly over the span, not over leading-edge arc length
station_indices now places its targets along the quarter-chord line with the chordwise component dropped, so a tip whose leading edge runs aft no longer gathers sections, and wingtip_distance is a spanwise inset. march_edges drops its arclen field, which only station_indices read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 246237a commit 9af0690

7 files changed

Lines changed: 56 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
- The Makie `plot!` methods for a `Panel` or a `BodyAerodynamics` return a
99
`Vector{Makie.AbstractPlot}` instead of a `Vector{Any}`; for a `BodyAerodynamics`
1010
drawn as flat panels it is one flat list rather than a list per panel.
11+
- `obj_to_yaml` and `perpendicular_sections` spread the sections evenly over the span,
12+
measured along the quarter-chord line without its chordwise component, instead of
13+
over leading-edge arc length, and `wingtip_distance` is that spanwise length. A tip
14+
whose leading edge runs aft no longer gathers sections into its last centimetres.
1115

1216
### Fixed
1317

docs/src/airfoil_pipeline.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The conversion runs four stages per spanwise station:
2727
## 1. Slice
2828

2929
[`perpendicular_sections`](@ref VortexStepMethod.ObjAdapter.perpendicular_sections)
30-
places `n_sections` stations at equal leading-edge arc-length intervals and cuts the
30+
places `n_sections` stations evenly over the span and cuts the
3131
mesh *perpendicular to the local span*, rather than along a fixed global plane. On a
3232
curved kite tip a fixed-plane cut would smear the profile out and exaggerate the
3333
chord; a perpendicular cut keeps each airfoil undistorted. Each slice comes back as a
@@ -113,8 +113,8 @@ For each unique airfoil id `j`, `obj_to_yaml` writes into `output_dir`:
113113
directory
114114

115115
A tip that tapers to a point has no airfoil to slice, so the outermost stations stop at
116-
the last slice that still has a chord; `wingtip_distance` moves them a further arc length
117-
inboard when the slices just short of the tip are still too thin to analyse. A
116+
the last slice that still has a chord; `wingtip_distance` moves them a further spanwise
117+
length inboard when the slices just short of the tip are still too thin to analyse. A
118118
near-vanishing slice that does get through can shrink-wrap to an implausibly thick blob;
119119
such a degenerate section reuses its nearest valid neighbour's airfoil and polar while
120120
keeping its own edge positions, and a warning lists the reuse. All floats are rounded to millimetre

docs/src/settings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ wings:
5353
n_bins: 60 # leading-edge stations marched across the span
5454
# rows of the mesh-to-slicer rotation, whose x = chord, y = span, z = up
5555
rotation: [[0, 0, -1], [-1, 0, 0], [0, 1, 0]]
56-
wingtip_distance: 0.0 # arc length the outermost sections stop short [m]
56+
wingtip_distance: 0.0 # span the outermost sections stop short [m]
5757
clearance: 0.006 # shrink-wrap offset outside the cloud [chord fraction]
5858
min_concave_radius: 0.02 # shrink-wrap rolling-ball radius [chord fraction]
5959

src/obj_adapter/obj_slice.jl

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -270,16 +270,16 @@ function slice_mesh_at_plane(vertices, faces, point, normal; tol=1e-6)
270270
end
271271

272272
"""
273-
march_edges(vertices, faces; step) -> (; le, te, point, tangent, arclen)
273+
march_edges(vertices, faces; step) -> (; le, te, point, tangent)
274274
275275
March the leading edge outward from mid-span in both directions in steps of arc
276276
length `step`. Each cut is a vertical spanwise plane (both the chordwise and vertical
277277
components of the running LE tangent dropped from the normal) so a tip that curls
278278
downward can't tilt the plane toward horizontal, where its min-chord "LE" pick would
279279
jump across the wing. Marching stops when the leading edge stops advancing spanwise.
280280
Cuts sample mesh *edges*, so the picks are robust to vertex density. Returns, ordered
281-
along the span, the LE/TE points, each cut's plane origin and tangent, and the
282-
cumulative LE arc length. Build the airfoil for a chosen station with `build_section`.
281+
along the span, the LE/TE points and each cut's plane origin and tangent. Build the
282+
airfoil for a chosen station with `build_section`.
283283
"""
284284
function march_edges(vertices, faces; step)
285285
ys = [v[2] for v in vertices]
@@ -318,7 +318,7 @@ function march_edges(vertices, faces; step)
318318
probe_mid = prev_le .+ mid .* tangent
319319
here = cut(probe_mid, tangent)
320320
# Reject a near-degenerate tip slice whose min-chord "LE" has jumped
321-
# chordwise (an artifact that would skew the leading-edge arc length).
321+
# chordwise (an artifact that would misplace the tip station).
322322
valid = here !== nothing &&
323323
abs(here.le[1] - prev_le[1]) < step &&
324324
build_section(vertices, faces, here.le, here.te,
@@ -340,12 +340,8 @@ function march_edges(vertices, faces; step)
340340

341341
center = (; mid_cut.le, mid_cut.te, point=[0.0, y_mid, 0.0], tangent=[0.0, 1.0, 0.0])
342342
rows = vcat(reverse(march(-1.0)), [center], march(1.0))
343-
arclen = zeros(length(rows))
344-
for i in 2:length(rows)
345-
arclen[i] = arclen[i-1] + norm(rows[i].le - rows[i-1].le)
346-
end
347343
return (; le=[r.le for r in rows], te=[r.te for r in rows],
348-
point=[r.point for r in rows], tangent=[r.tangent for r in rows], arclen)
344+
point=[r.point for r in rows], tangent=[r.tangent for r in rows])
349345
end
350346

351347
"""
@@ -429,12 +425,12 @@ end
429425
430426
Extract `n_sections` airfoil cross-sections following a curved or swept span. The
431427
leading edge is marched into `n_bins` stations (`march_edges`); the airfoil is
432-
built (`build_section`) at the marched station nearest each equal
433-
**leading-edge arc-length** target. Each section is
428+
built (`build_section`) at the marched station nearest each of `n_sections` targets
429+
spread evenly over the span ([`station_indices`](@ref)). Each section is
434430
`(; LE_point, TE_point, span_dir, contour3d, x_airfoil, y_airfoil)`.
435431
436-
Stations closed to a point at the tips are skipped ([`station_indices`](@ref)), and
437-
`wingtip_distance` insets the outermost sections a further arc length.
432+
Stations closed to a point at the tips are skipped, and `wingtip_distance` [m]
433+
insets the outermost sections a further spanwise length.
438434
439435
The slicer assumes `x` = chordwise, `y` = spanwise, `z` = up. Pass a `3×3` rotation
440436
matrix to reorient a mesh stored in another convention before slicing.
@@ -455,19 +451,24 @@ end
455451
"""
456452
station_indices(march, n; wingtip_distance=0.0, min_chord_frac=0.01) -> Vector{Int}
457453
458-
Indices of the [`march_edges`](@ref) stations nearest `n` targets spread over the
459-
leading-edge arc length. Stations whose chord has closed to less than
460-
`min_chord_frac` of the longest one are left out of that range first, so a wing
461-
tapering to a point puts its outermost sections on the last stations that still
462-
have an airfoil to slice rather than on the point itself. The remaining first and
463-
last targets sit a further `wingtip_distance` (arc length) inboard.
454+
Indices of the [`march_edges`](@ref) stations nearest `n` targets spread evenly over
455+
the spanwise length of the quarter-chord line: its arc length with the chordwise `x`
456+
component dropped. Stations whose chord has closed to less than `min_chord_frac` of
457+
the longest one are left out of that range first, so a wing tapering to a point puts
458+
its outermost sections on the last stations that still have an airfoil to slice. The
459+
remaining first and last targets sit a further `wingtip_distance` [m] inboard.
464460
"""
465461
function station_indices(march, n; wingtip_distance=0.0, min_chord_frac=0.01)
466462
chords = [norm(te .- le) for (le, te) in zip(march.le, march.te)]
467463
usable = findall((min_chord_frac * maximum(chords)), chords)
468-
arclen = march.arclen
469-
inner, outer = arclen[first(usable)], arclen[last(usable)]
464+
quarter_chord = [le .+ 0.25 .* (te .- le) for (le, te) in zip(march.le, march.te)]
465+
span = zeros(length(quarter_chord))
466+
for i in 2:length(span)
467+
step = quarter_chord[i] .- quarter_chord[i-1]
468+
span[i] = span[i-1] + hypot(step[2], step[3])
469+
end
470+
inner, outer = span[first(usable)], span[last(usable)]
470471
d = clamp(wingtip_distance, 0.0, (outer - inner) / 2)
471-
n == 1 && return [argmin(abs.(arclen .- (inner + outer) / 2))]
472-
return [argmin(abs.(arclen .- t)) for t in range(inner + d, outer - d, n)]
472+
n == 1 && return [argmin(abs.(span .- (inner + outer) / 2))]
473+
return [argmin(abs.(span .- t)) for t in range(inner + d, outer - d, n)]
473474
end

src/obj_adapter/obj_to_yaml.jl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,13 @@ end
4242
4343
Convert a 3D wing `.obj` mesh to the native YAML geometry route.
4444
45-
Stations are placed at equal leading-edge arc-length intervals and sliced
45+
Stations are spread evenly over the span ([`station_indices`](@ref)) and sliced
4646
perpendicular to the local span (see [`perpendicular_sections`](@ref)), which
4747
keeps the airfoil undistorted near curved tips; each shape is then shrink-wrapped
4848
into a clean airfoil and evaluated with `aero_solver`. The leading edge is marched
49-
into `n_bins` stations. A tip that tapers to a
50-
point carries no airfoil, so the outermost stations stop at the last slice that
51-
still has a chord ([`station_indices`](@ref)); `wingtip_distance` moves them a
52-
further arc length inboard.
49+
into `n_bins` stations. A tip that tapers to a point carries no airfoil, so the
50+
outermost stations stop at the last slice that still has a chord; `wingtip_distance`
51+
moves them a further spanwise length inboard.
5352
5453
`aero_solver` selects the 2D-airfoil backend: [`NeuralFoilSolver`](@ref) (default,
5554
fast) or [`XFoilSolver`](@ref) (viscous panel code); pass `aero_solver=XFoilSolver()`

src/settings.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ slices as an unconfigured call.
3838
trace (default `60`).
3939
- `rotation`: Rows of the mesh-to-slicer rotation, which brings the mesh into the
4040
slicer's convention of x = chord, y = span, z = up (default the identity).
41-
- `wingtip_distance`: Arc length [m] the outermost sections stop short of each tip
41+
- `wingtip_distance`: Spanwise length [m] the outermost sections stop short of each tip
4242
(default `0.0`).
4343
- `clearance`: Shrink-wrap offset [chord fraction] the contour holds outside every
4444
cloud point, and the radius its convex corners are rounded at (default

test/obj_adapter/test_obj_adapter.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@ obj_path = normpath(joinpath(@__DIR__, "..", "..",
2828
end
2929
end
3030

31+
@testset "station_indices spreads sections evenly in span past a raked tip" begin
32+
# Straight LE over |y| ≤ 1, then tip caps whose LE runs 40 mm aft per 3 mm span.
33+
cap = [(1.0 + 0.003k, 0.04k) for k in 1:20]
34+
stations = vcat(reverse([(-y, x) for (y, x) in cap]),
35+
[(y, 0.0) for y in -1.0:0.05:1.0], cap)
36+
march = (; le=[[x, y, 0.0] for (y, x) in stations],
37+
te=[[1.0, y, 0.0] for (y, _) in stations])
38+
half_span = 1.06
39+
40+
y = [march.le[i][2] for i in ObjAdapter.station_indices(march, 9)]
41+
@test all(isapprox.(diff(y), 2half_span / 8; atol=0.05))
42+
43+
wingtip_distance = 0.3
44+
y = [march.le[i][2]
45+
for i in ObjAdapter.station_indices(march, 9; wingtip_distance)]
46+
@test y[1] -(half_span - wingtip_distance) atol=0.05
47+
@test y[end] half_span - wingtip_distance atol=0.05
48+
end
49+
3150
@testset "obj_to_yaml (alpha,delta) matrices -> loadable Wing (NeuralFoil)" begin
3251
@test isfile(yaml)
3352
@test isfile(joinpath(out, "polars", "1.csv"))

0 commit comments

Comments
 (0)