From b7a47bf899f2b4a8f40bf939454ac5ae22847f90 Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:52:44 +0200 Subject: [PATCH] Refuse a contour XFoil's panel code cannot take A self-crossing contour can reach a bare Fortran STOP inside XFoil, which ends the Julia process with exit code 0 and no exception. `analyze_sweep` now checks the contour before `set_coordinates` and throws `ArgumentError` instead, so a section XFoil has no solution for is one failed sweep rather than the end of the session. The same check covers a contour with more nodes than XFoil's panel arrays hold: ABCOPY refused it and left the previously loaded airfoil in place, so every angle came back solved on the wrong shape. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018rHHzpuPmi8pBjVk3r5QZD --- CHANGELOG.md | 11 ++++++ docs/src/private_functions.md | 8 +++++ src/airfoil_aero/airfoil_solvers/common.jl | 35 +++++++++++++++++++ .../airfoil_solvers/xfoil_solver.jl | 21 +++++++++++ test/airfoil_aero/test_airfoil_aero.jl | 22 +++++++++++- 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 532b0816..dd2f327d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased + +### Fixed + +- `analyze_sweep(::XFoilSolver, ...)` throws `ArgumentError` for a contour XFoil's + panel code cannot take, rather than handing it to the Fortran. A self-crossing + contour could reach a bare `STOP` there, ending the Julia process with exit code 0 + and no exception; a contour with more nodes than XFoil's panel arrays hold was + refused by XFoil, which then solved every angle on whichever airfoil was loaded + before and returned it as this one. + ## VortexStepMethod v5.1.1 2026-09-12 ### Fixed diff --git a/docs/src/private_functions.md b/docs/src/private_functions.md index d7807c42..35cc72a8 100644 --- a/docs/src/private_functions.md +++ b/docs/src/private_functions.md @@ -167,6 +167,14 @@ resample_arc smoothed_curvature ``` +### Section solver preconditions +```@docs +validate_xfoil_contour +crossing_panels +segments_cross +side_of_line +``` + ### NeuralFoil network ```@docs load_neuralfoil_model diff --git a/src/airfoil_aero/airfoil_solvers/common.jl b/src/airfoil_aero/airfoil_solvers/common.jl index 60c79409..b4e7725e 100644 --- a/src/airfoil_aero/airfoil_solvers/common.jl +++ b/src/airfoil_aero/airfoil_solvers/common.jl @@ -116,6 +116,41 @@ function deform_section(x, y, delta; crease_frac=0.9, thickness_frac=1.0, return DeformedSection(kulfan, xd, yd) end +""" + side_of_line(a, b, p) -> Float64 + +Twice the signed area of the triangle `a`, `b`, `p`: positive with `p` left of the +line from `a` to `b`, negative right of it, zero on it. +""" +side_of_line(a, b, p) = (b[1] - a[1]) * (p[2] - a[2]) - (b[2] - a[2]) * (p[1] - a[1]) + +""" + segments_cross(p, q, r, s) -> Bool + +Whether the segments `p`-`q` and `r`-`s` cross properly, each strictly separating +the other's endpoints. Touching at an endpoint or lying along each other does not +count. +""" +segments_cross(p, q, r, s) = + side_of_line(p, q, r) * side_of_line(p, q, s) < 0 && + side_of_line(r, s, p) * side_of_line(r, s, q) < 0 + +""" + crossing_panels(x, y) -> Tuple{Int,Int} or nothing + +The first pair of non-neighbouring panels of the closed contour `(x, y)` that +cross, or `nothing` when the contour is a simple closed curve. +""" +function crossing_panels(x, y) + nodes = collect(zip(x, y)) + last_panel = length(nodes) - 1 + for i in 1:last_panel, j in (i + 2):last_panel + i == 1 && j == last_panel && continue + segments_cross(nodes[i], nodes[i+1], nodes[j], nodes[j+1]) && return (i, j) + end + return nothing +end + """ analyze_sweep(solver, def, alpha_range, Re) -> Vector{SectionSolution} diff --git a/src/airfoil_aero/airfoil_solvers/xfoil_solver.jl b/src/airfoil_aero/airfoil_solvers/xfoil_solver.jl index 28ff516e..a5b9204c 100644 --- a/src/airfoil_aero/airfoil_solvers/xfoil_solver.jl +++ b/src/airfoil_aero/airfoil_solvers/xfoil_solver.jl @@ -28,6 +28,23 @@ runs (`ncrit=9`, `max_iter=100`, incompressible) so the two backends are compara repanel::Bool = false end +""" + validate_xfoil_contour(def::DeformedSection) + +Enforce what XFoil's panel code needs of `def`'s coordinates: no more nodes than its +panel arrays hold, and no two panels crossing. Throws `ArgumentError` on a violation. +""" +function validate_xfoil_contour(def::DeformedSection) + max_nodes = Xfoil.IQX - 5 + length(def.x) <= max_nodes || throw(ArgumentError( + "XFoil holds $max_nodes panel nodes; this contour has $(length(def.x)).")) + crossing = crossing_panels(def.x, def.y) + isnothing(crossing) || throw(ArgumentError( + "XFoil needs a contour that does not cross itself; panels $(crossing[1]) " * + "and $(crossing[2]) of this one do.")) + return nothing +end + """ analyze_sweep(solver::XFoilSolver, def, alpha_range, Re) -> Vector{SectionSolution} @@ -37,8 +54,12 @@ reinit at each side for convergence. Each converged angle reads the surface pres (`Xfoil.cpdump`) and the boundary layer (`Xfoil.bldump`, giving `cf` and the node coordinates) at the same panel nodes. Non-converged angles yield empty node arrays and `NaN` confidence. + +Throws `ArgumentError` for a contour XFoil's panel code cannot take, see +[`validate_xfoil_contour`](@ref). """ function analyze_sweep(solver::XFoilSolver, def::DeformedSection, alpha_range, Re) + validate_xfoil_contour(def) Xfoil.set_coordinates(def.x, def.y) solver.repanel && Xfoil.pane(npan=solver.npan) sols = Vector{SectionSolution}(undef, length(alpha_range)) diff --git a/test/airfoil_aero/test_airfoil_aero.jl b/test/airfoil_aero/test_airfoil_aero.jl index ac4b9363..90700a41 100644 --- a/test/airfoil_aero/test_airfoil_aero.jl +++ b/test/airfoil_aero/test_airfoil_aero.jl @@ -4,7 +4,8 @@ import VortexStepMethod using VortexStepMethod.AirfoilAero: KulfanParameters, LeastSquaresFit, ShrinkWrap, shrink_wrap, fit_kulfan_parameters, kulfan_to_coordinates, neuralfoil_aero, class_function, bernstein_basis, - leading_edge_basis, normalize_airfoil + leading_edge_basis, normalize_airfoil, crossing_panels, + DeformedSection, XFoilSolver, analyze_sweep, Xfoil using VortexStepMethod: SectionAero, section_surface, read_section_aero using VortexStepMethod.AirfoilAero: write_section_aero @@ -299,3 +300,22 @@ end @test maximum(abs, collect(extrema(written_y)) .- collect(extrema(fitted_y))) < 1e-4 end + +@testset "XFoil refuses a contour it has no solution for" begin + clean = KulfanParameters(fill(0.15, 8), fill(-0.15, 8), 0.0, 0.0) + x, y = collect.(kulfan_to_coordinates(clean; n_points=60)) + alphas = deg2rad.([0.0]) + @test isnothing(crossing_panels(x, y)) + + # the upper surface driven through the lower one over a stretch of the chord + folded = copy(y) + folded[20:40] .= -3 .* folded[20:40] + @test !isnothing(crossing_panels(x, folded)) + @test_throws ArgumentError analyze_sweep(XFoilSolver(), + DeformedSection(clean, x, folded), alphas, 1e6) + + crowded_x, crowded_y = collect.(kulfan_to_coordinates(clean; n_points=200)) + @test length(crowded_x) > Xfoil.IQX - 5 + @test_throws ArgumentError analyze_sweep(XFoilSolver(), + DeformedSection(clean, crowded_x, crowded_y), alphas, 1e6) +end