Skip to content

Commit a324968

Browse files
authored
Merge pull request #376 from OpenSourceAWE/agent/361-march-edges-gives-each-station-the-previ
march_edges takes each station's tangent from its neighbours, so no cut near a closing tip grazes the surface
2 parents 1b72901 + f117ec9 commit a324968

4 files changed

Lines changed: 105 additions & 61 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
from its own wing's `spanwise_direction`, not the first wing's. `solve` computes
8383
`wing_span` and `aspect_ratio_projected` from the extent of all wings along the
8484
first wing's span, through the new `calculate_span(wings, spanwise_direction)`.
85+
- `perpendicular_sections` and `obj_to_yaml` orient each cut by the leading edge through
86+
the neighbouring stations, not by the step that reached the station, which near a
87+
closing tip grazed the surface and returned an 80%-thick section. Every section moves
88+
slightly; regenerate existing geometry to benefit.
8589
- `ELLIPTIC` initial circulation works on a body with more than one wing, where it threw
8690
an `ArgumentError`: each wing gets an ellipse over its own span, along its own
8791
`spanwise_direction` and centred on its own mid-span, also for a single wing off y = 0.

docs/src/private_functions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ densify_contour
260260
create_interpolations
261261
find_circle_center_and_radius
262262
march_edges
263+
cut_station
264+
tip_station
265+
march_stations
263266
airfoils_from_yaml
264267
write_geometry_yaml
265268
resolve_aero_geometry

src/obj_adapter/obj_slice.jl

Lines changed: 82 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -269,79 +269,100 @@ function slice_mesh_at_plane(vertices, faces, point, normal; tol=1e-6)
269269
return segments
270270
end
271271

272+
"""
273+
cut_station(vertices, faces, point, tangent) -> (; le, te) or nothing
274+
275+
Cut the mesh with the vertical spanwise plane through `point` whose normal is the
276+
spanwise component of `tangent`, and return the crossings with the least and greatest
277+
`x` as the leading and trailing edge; `nothing` where the plane misses the mesh.
278+
"""
279+
function cut_station(vertices, faces, point, tangent)
280+
segments = slice_mesh_at_plane(vertices, faces, point,
281+
normalize([0.0, tangent[2], 0.0]))
282+
isempty(segments) && return nothing
283+
crossings = Vector{Float64}[]
284+
for (start_point, end_point) in segments
285+
push!(crossings, start_point)
286+
push!(crossings, end_point)
287+
end
288+
return (; le=argmin(crossing -> crossing[1], crossings),
289+
te=argmax(crossing -> crossing[1], crossings))
290+
end
291+
292+
"""
293+
tip_station(vertices, faces, prev_le, tangent, step) -> (; le, te, point) or nothing
294+
295+
Bisect the step of length `step` from `prev_le` along `tangent` for the outermost cut
296+
that still yields an airfoil section whose leading edge stays within `step` of
297+
`prev_le` chordwise; `nothing` where no such cut exists.
298+
"""
299+
function tip_station(vertices, faces, prev_le, tangent, step)
300+
lo, hi, tip = 0.0, step, nothing
301+
for _ in 1:24
302+
mid = (lo + hi) / 2
303+
probe = prev_le .+ mid .* tangent
304+
here = cut_station(vertices, faces, probe, tangent)
305+
valid = here !== nothing &&
306+
abs(here.le[1] - prev_le[1]) < step &&
307+
build_section(vertices, faces, here.le, here.te, probe, tangent) !== nothing
308+
valid ? (lo = mid; tip = (; here.le, here.te, point=probe)) : (hi = mid)
309+
end
310+
return tip
311+
end
312+
313+
"""
314+
march_stations(vertices, faces, start_le, step, direction) -> Vector{NamedTuple}
315+
316+
March the leading edge from `start_le` in steps of arc length `step` towards `+y` for
317+
`direction = 1.0` or `-y` for `-1.0`, returning one `(; le, te, point)` per station,
318+
the tip station last. Stops when the leading edge stops advancing spanwise.
319+
"""
320+
function march_stations(vertices, faces, start_le, step, direction)
321+
rows = NamedTuple[]
322+
prev_le = start_le
323+
tangent = [0.0, direction, 0.0]
324+
for _ in 1:10_000
325+
probe = prev_le .+ step .* tangent
326+
found = cut_station(vertices, faces, probe, tangent)
327+
if found === nothing
328+
tip = tip_station(vertices, faces, prev_le, tangent, step)
329+
tip === nothing || push!(rows, tip)
330+
break
331+
end
332+
le_step = found.le .- prev_le
333+
(norm(le_step) < 1e-9 || le_step[2] * direction <= 0.0) && break
334+
push!(rows, (; found.le, found.te, point=probe))
335+
tangent = normalize(le_step)
336+
prev_le = found.le
337+
end
338+
return rows
339+
end
340+
272341
"""
273342
march_edges(vertices, faces; step) -> (; le, te, point, tangent)
274343
275-
March the leading edge outward from mid-span in both directions in steps of arc
276-
length `step`. Each cut is a vertical spanwise plane (both the chordwise and vertical
277-
components of the running LE tangent dropped from the normal) so a tip that curls
278-
downward can't tilt the plane toward horizontal, where its min-chord "LE" pick would
279-
jump across the wing. Marching stops when the leading edge stops advancing spanwise.
280-
Cuts sample mesh *edges*, so the picks are robust to vertex density. Returns, ordered
281-
along the span, the LE/TE points and each cut's plane origin and tangent. Build the
344+
Cut the mesh at mid-span and march the leading edge outward from there in both
345+
directions with `march_stations`. Returns, ordered along the span, the LE/TE points,
346+
each cut's plane origin, and the LE tangent: the central difference over the
347+
neighbouring stations, where the end stations reuse their inner neighbour's. Build the
282348
airfoil for a chosen station with `build_section`.
283349
"""
284350
function march_edges(vertices, faces; step)
285351
ys = [v[2] for v in vertices]
286352
y_min, y_max = extrema(ys)
287353
y_mid = (y_min + y_max) / 2
288354

289-
# Vertical spanwise plane (z dropped): a tip curl mustn't tilt the cut horizontal.
290-
cut(point, tangent) = begin
291-
segments = slice_mesh_at_plane(vertices, faces, point,
292-
normalize([0.0, tangent[2], 0.0]))
293-
isempty(segments) && return nothing
294-
crossings = Vector{Float64}[]
295-
for (start_point, end_point) in segments
296-
push!(crossings, start_point)
297-
push!(crossings, end_point)
298-
end
299-
return (; le=argmin(pt -> pt[1], crossings), te=argmax(pt -> pt[1], crossings))
300-
end
301-
302-
mid_cut = cut([0.0, y_mid, 0.0], [0.0, 1.0, 0.0])
355+
mid_cut = cut_station(vertices, faces, [0.0, y_mid, 0.0], [0.0, 1.0, 0.0])
303356
mid_cut === nothing && error("Could not slice mid-span; check mesh / rotation.")
304357

305-
march(direction) = begin
306-
rows = NamedTuple[]
307-
prev_le = mid_cut.le
308-
tangent = [0.0, direction, 0.0]
309-
for _ in 1:10_000
310-
probe = prev_le .+ step .* tangent
311-
found = cut(probe, tangent)
312-
if found === nothing
313-
# Bisect the last step to land the tip station on the outermost cut
314-
# that still yields a valid (non-degenerate) airfoil section.
315-
lo, hi, tip = 0.0, step, nothing
316-
for _ in 1:24
317-
mid = (lo + hi) / 2
318-
probe_mid = prev_le .+ mid .* tangent
319-
here = cut(probe_mid, tangent)
320-
# Reject a near-degenerate tip slice whose min-chord "LE" has jumped
321-
# chordwise (an artifact that would misplace the tip station).
322-
valid = here !== nothing &&
323-
abs(here.le[1] - prev_le[1]) < step &&
324-
build_section(vertices, faces, here.le, here.te,
325-
probe_mid, tangent) !== nothing
326-
valid ? (lo = mid; tip = (; here.le, here.te, point=probe_mid)) :
327-
(hi = mid)
328-
end
329-
tip === nothing || push!(rows, (; tip.le, tip.te, tip.point, tangent))
330-
break
331-
end
332-
le_step = found.le .- prev_le
333-
(norm(le_step) < 1e-9 || le_step[2] * direction <= 0.0) && break
334-
push!(rows, (; found.le, found.te, point=probe, tangent))
335-
tangent = normalize(le_step)
336-
prev_le = found.le
337-
end
338-
return rows
339-
end
340-
341-
center = (; mid_cut.le, mid_cut.te, point=[0.0, y_mid, 0.0], tangent=[0.0, 1.0, 0.0])
342-
rows = vcat(reverse(march(-1.0)), [center], march(1.0))
343-
return (; le=[r.le for r in rows], te=[r.te for r in rows],
344-
point=[r.point for r in rows], tangent=[r.tangent for r in rows])
358+
center = (; mid_cut.le, mid_cut.te, point=[0.0, y_mid, 0.0])
359+
negative_y = march_stations(vertices, faces, mid_cut.le, step, -1.0)
360+
positive_y = march_stations(vertices, faces, mid_cut.le, step, 1.0)
361+
rows = vcat(reverse(negative_y), [center], positive_y)
362+
le = [row.le for row in rows]
363+
middle = clamp.(eachindex(le), 2, length(le) - 1)
364+
tangent = [normalize(le[j + 1] .- le[j - 1]) for j in middle]
365+
return (; le, te=[row.te for row in rows], point=[row.point for row in rows], tangent)
345366
end
346367

347368
"""

test/obj_adapter/test_obj_adapter.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ obj_path = normpath(joinpath(@__DIR__, "..", "..",
2929
end
3030
end
3131

32+
@testset "every marched station cuts across the surface, closing tips included" begin
33+
vertices, faces = read_faces(obj_path)
34+
ys = [v[2] for v in vertices]
35+
for n_bins in (60, 90)
36+
march = ObjAdapter.march_edges(vertices, faces;
37+
step=(maximum(ys) - minimum(ys)) / n_bins)
38+
for i in eachindex(march.le)
39+
section = ObjAdapter.build_section(vertices, faces, march.le[i],
40+
march.te[i], march.point[i], march.tangent[i])
41+
depth = section === nothing ? Inf :
42+
maximum(section.y_airfoil) - minimum(section.y_airfoil)
43+
@test depth < 0.3
44+
end
45+
end
46+
end
47+
3248
@testset "station_indices spreads sections evenly in span past a raked tip" begin
3349
# Straight LE over |y| ≤ 1, then tip caps whose LE runs 40 mm aft per 3 mm span.
3450
cap = [(1.0 + 0.003k, 0.04k) for k in 1:20]

0 commit comments

Comments
 (0)