Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- 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.
- `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.

### Fixed

Expand Down
6 changes: 3 additions & 3 deletions docs/src/airfoil_pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The conversion runs four stages per spanwise station:
## 1. Slice

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

A tip that tapers to a point has no airfoil to slice, so the outermost stations stop at
the last slice that still has a chord; `wingtip_distance` moves them a further arc length
inboard when the slices just short of the tip are still too thin to analyse. A
the last slice that still has a chord; `wingtip_distance` moves them a further spanwise
length inboard when the slices just short of the tip are still too thin to analyse. A
near-vanishing slice that does get through can shrink-wrap to an implausibly thick blob;
such a degenerate section reuses its nearest valid neighbour's airfoil and polar while
keeping its own edge positions, and a warning lists the reuse. All floats are rounded to millimetre
Expand Down
2 changes: 1 addition & 1 deletion docs/src/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ wings:
n_bins: 60 # leading-edge stations marched across the span
# rows of the mesh-to-slicer rotation, whose x = chord, y = span, z = up
rotation: [[0, 0, -1], [-1, 0, 0], [0, 1, 0]]
wingtip_distance: 0.0 # arc length the outermost sections stop short [m]
wingtip_distance: 0.0 # span the outermost sections stop short [m]
clearance: 0.006 # shrink-wrap offset outside the cloud [chord fraction]
min_concave_radius: 0.02 # shrink-wrap rolling-ball radius [chord fraction]

Expand Down
47 changes: 24 additions & 23 deletions src/obj_adapter/obj_slice.jl
Original file line number Diff line number Diff line change
Expand Up @@ -270,16 +270,16 @@ function slice_mesh_at_plane(vertices, faces, point, normal; tol=1e-6)
end

"""
march_edges(vertices, faces; step) -> (; le, te, point, tangent, arclen)
march_edges(vertices, faces; step) -> (; le, te, point, tangent)

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

center = (; mid_cut.le, mid_cut.te, point=[0.0, y_mid, 0.0], tangent=[0.0, 1.0, 0.0])
rows = vcat(reverse(march(-1.0)), [center], march(1.0))
arclen = zeros(length(rows))
for i in 2:length(rows)
arclen[i] = arclen[i-1] + norm(rows[i].le - rows[i-1].le)
end
return (; le=[r.le for r in rows], te=[r.te for r in rows],
point=[r.point for r in rows], tangent=[r.tangent for r in rows], arclen)
point=[r.point for r in rows], tangent=[r.tangent for r in rows])
end

"""
Expand Down Expand Up @@ -429,12 +425,12 @@ end

Extract `n_sections` airfoil cross-sections following a curved or swept span. The
leading edge is marched into `n_bins` stations (`march_edges`); the airfoil is
built (`build_section`) at the marched station nearest each equal
**leading-edge arc-length** target. Each section is
built (`build_section`) at the marched station nearest each of `n_sections` targets
spread evenly over the span ([`station_indices`](@ref)). Each section is
`(; LE_point, TE_point, span_dir, contour3d, x_airfoil, y_airfoil)`.

Stations closed to a point at the tips are skipped ([`station_indices`](@ref)), and
`wingtip_distance` insets the outermost sections a further arc length.
Stations closed to a point at the tips are skipped, and `wingtip_distance` [m]
insets the outermost sections a further spanwise length.

The slicer assumes `x` = chordwise, `y` = spanwise, `z` = up. Pass a `3×3` rotation
matrix to reorient a mesh stored in another convention before slicing.
Expand All @@ -455,19 +451,24 @@ end
"""
station_indices(march, n; wingtip_distance=0.0, min_chord_frac=0.01) -> Vector{Int}

Indices of the [`march_edges`](@ref) stations nearest `n` targets spread over the
leading-edge arc length. Stations whose chord has closed to less than
`min_chord_frac` of the longest one are left out of that range first, so a wing
tapering to a point puts its outermost sections on the last stations that still
have an airfoil to slice rather than on the point itself. The remaining first and
last targets sit a further `wingtip_distance` (arc length) inboard.
Indices of the [`march_edges`](@ref) stations nearest `n` targets spread evenly over
the spanwise length of the quarter-chord line: its arc length with the chordwise `x`
component dropped. Stations whose chord has closed to less than `min_chord_frac` of
the longest one are left out of that range first, so a wing tapering to a point puts
its outermost sections on the last stations that still have an airfoil to slice. The
remaining first and last targets sit a further `wingtip_distance` [m] inboard.
"""
function station_indices(march, n; wingtip_distance=0.0, min_chord_frac=0.01)
chords = [norm(te .- le) for (le, te) in zip(march.le, march.te)]
usable = findall(≥(min_chord_frac * maximum(chords)), chords)
arclen = march.arclen
inner, outer = arclen[first(usable)], arclen[last(usable)]
quarter_chord = [le .+ 0.25 .* (te .- le) for (le, te) in zip(march.le, march.te)]
span = zeros(length(quarter_chord))
for i in 2:length(span)
step = quarter_chord[i] .- quarter_chord[i-1]
span[i] = span[i-1] + hypot(step[2], step[3])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR: The card says this matches what refine_mesh_for_linear_cosine_distribution! measures, but that function (and compute_refined_section_interpolation!) uses full 3D quarter-chord length with x included. The repo now has two different ideas of span position, and refinement still places panels by the length this PR stops using. Either correct the card and say so, or explain why the two may differ.

end
inner, outer = span[first(usable)], span[last(usable)]
d = clamp(wingtip_distance, 0.0, (outer - inner) / 2)
n == 1 && return [argmin(abs.(arclen .- (inner + outer) / 2))]
return [argmin(abs.(arclen .- t)) for t in range(inner + d, outer - d, n)]
n == 1 && return [argmin(abs.(span .- (inner + outer) / 2))]
return [argmin(abs.(span .- t)) for t in range(inner + d, outer - d, n)]
end
9 changes: 4 additions & 5 deletions src/obj_adapter/obj_to_yaml.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,13 @@ end

Convert a 3D wing `.obj` mesh to the native YAML geometry route.

Stations are placed at equal leading-edge arc-length intervals and sliced
Stations are spread evenly over the span ([`station_indices`](@ref)) and sliced
perpendicular to the local span (see [`perpendicular_sections`](@ref)), which
keeps the airfoil undistorted near curved tips; each shape is then shrink-wrapped
into a clean airfoil and evaluated with `aero_solver`. The leading edge is marched
into `n_bins` stations. A tip that tapers to a
point carries no airfoil, so the outermost stations stop at the last slice that
still has a chord ([`station_indices`](@ref)); `wingtip_distance` moves them a
further arc length inboard.
into `n_bins` stations. A tip that tapers to a point carries no airfoil, so the
outermost stations stop at the last slice that still has a chord; `wingtip_distance`
moves them a further spanwise length inboard.

`aero_solver` selects the 2D-airfoil backend: [`NeuralFoilSolver`](@ref) (default,
fast) or [`XFoilSolver`](@ref) (viscous panel code); pass `aero_solver=XFoilSolver()`
Expand Down
2 changes: 1 addition & 1 deletion src/settings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ slices as an unconfigured call.
trace (default `60`).
- `rotation`: Rows of the mesh-to-slicer rotation, which brings the mesh into the
slicer's convention of x = chord, y = span, z = up (default the identity).
- `wingtip_distance`: Arc length [m] the outermost sections stop short of each tip
- `wingtip_distance`: Spanwise length [m] the outermost sections stop short of each tip
(default `0.0`).
- `clearance`: Shrink-wrap offset [chord fraction] the contour holds outside every
cloud point, and the radius its convex corners are rounded at (default
Expand Down
19 changes: 19 additions & 0 deletions test/obj_adapter/test_obj_adapter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ obj_path = normpath(joinpath(@__DIR__, "..", "..",
end
end

@testset "station_indices spreads sections evenly in span past a raked tip" begin
# Straight LE over |y| ≤ 1, then tip caps whose LE runs 40 mm aft per 3 mm span.
cap = [(1.0 + 0.003k, 0.04k) for k in 1:20]
stations = vcat(reverse([(-y, x) for (y, x) in cap]),
[(y, 0.0) for y in -1.0:0.05:1.0], cap)
march = (; le=[[x, y, 0.0] for (y, x) in stations],
te=[[1.0, y, 0.0] for (y, _) in stations])
half_span = 1.06

y = [march.le[i][2] for i in ObjAdapter.station_indices(march, 9)]
@test all(isapprox.(diff(y), 2half_span / 8; atol=0.05))

wingtip_distance = 0.3
y = [march.le[i][2]
for i in ObjAdapter.station_indices(march, 9; wingtip_distance)]
@test y[1] ≈ -(half_span - wingtip_distance) atol=0.05
@test y[end] ≈ half_span - wingtip_distance atol=0.05
end

@testset "obj_to_yaml (alpha,delta) matrices -> loadable Wing (NeuralFoil)" begin
@test isfile(yaml)
@test isfile(joinpath(out, "polars", "1.csv"))
Expand Down
Loading