Skip to content

Refuse a contour XFoil's panel code cannot take - #320

Open
1-Bort-1 wants to merge 1 commit into
mainfrom
agent/BeyondTheSim.jl-53
Open

1-Bort-1 wants to merge 1 commit into
mainfrom
agent/BeyondTheSim.jl-53

Conversation

@1-Bort-1

@1-Bort-1 1-Bort-1 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

TL;DR

analyze_sweep(::XFoilSolver, ...) checks the contour before handing it to XFoil and throws ArgumentError, because a self-crossing contour can reach a bare Fortran STOP in there and end the Julia process with exit code 0 and no exception. The same check catches a contour with more nodes than XFoil's panel arrays hold, which XFoil refused while leaving the previously loaded airfoil in place, so every angle came back solved on the wrong shape.

What was wrong

Found from 1-Bart-1/BeyondTheSim.jl#53: an example script ended its Julia process partway through, silently, exit code 0, no stacktrace — in a REPL it takes the session away, in a script it looks like a successful run that printed half its output. Bisecting the call, Xfoil.set_coordinates returns normally and Xfoil.solve_alpha(0.0, Re; reinit=true) never does. Exit 0 with nothing on stdout or stderr is a gfortran STOP with no message.

The contours that do it come out of deform_section, whose re-wrap of an already-wrapped thin section produces a curve whose upper and lower surfaces cross. That is not an airfoil, and a panel method has no solution for it, so which of them terminates the process and which returns is not something the caller can predict — of nine shapes sliced from one mesh section at nine rolling-ball radii, four returned and five took the process with them, while every geometric statistic I measured (node count, shortest panel, neighbouring-panel ratio, maximum turn, crossing count) put survivors and killers in the same range:

wrap radius shortest panel neighbour ratio max turn crossings XFoil
0.05 8.03e-4 2.20 45.0° 34 killed the process
0.10 7.38e-4 4.14 48.3° 34 returned
0.15 2.08e-4 16.65 180.0° 60 killed the process
0.20 2.66e-4 14.45 180.0° 58 killed the process
0.30 3.09e-4 13.54 180.0° 48 killed the process
0.40 4.07e-4 3.54 180.0° 29 returned
0.60 3.82e-4 6.29 180.0° 29 returned
0.80 3.90e-4 5.53 180.0° 35 returned
the stored .dat, one wrap of the same cloud 5.01e-4 7.50 32.3° 0 returned

The rows at 0.05 and 0.10 are the point: they agree on every column and disagree on life and death. So the guard cannot be a predictor of the STOP. What separates the one safe contour from the rest is that it is the only simple closed curve in the set, and that is the precondition a panel method actually has — which is what validate_xfoil_contour now enforces.

I do not claim this makes analyze_sweep proof against every contour XFoil might STOP on. It ends this class, which is the one a shrink wrap can produce, and it converts an unpredictable process death into a failure the caller can catch.

The second defect in the same function

While bisecting I found that a contour with more than Xfoil.IQX - 5 = 281 nodes is refused by XFoil's ABCOPY, which prints to stdout and returns, leaving the previously loaded airfoil in the panel arrays. analyze_sweep never looked, so it solved the sweep on whatever shape was loaded before and returned it as this one. Feeding a NACA 0012 at 299, 339, 399, 499 and 569 nodes gave cl = 0.5604994052186286 every time — the 279-node answer, to the last digit. Same function, same idea (say when XFoil did not take the contour), so it rides here.

Where I would push back

The real bug is upstream of this: deform_section's re-wrap should not produce a self-crossing contour in the first place, and until it stops doing so these sections have no XFoil polar at all rather than a poor one. That is the shrink wrap's own concern and would change what every generated polar means, so it is #321 rather than this diff.

The contour test is O(n²) in the node count — 239 nodes is ~28k segment pairs, run once per sweep against a viscous march of many angles. I did not benchmark it; it is not on any hot path I can find, and compare_live_polar's per-panel call is still one XFoil sweep per check.

Verification

  • Reproduced first: Xfoil.set_coordinates ok, then solve_alpha(0.0, Re; reinit=true) never returns — process gone, exit 0, no Julia error and no Fortran message
  • test/airfoil_aero/test_airfoil_aero.jl red before, green after (5/5 new assertions, 108/108 file, exit 0). Red was run with the source change stashed: the folded contour returned instead of throwing, and the 399-node contour printed Maximum number of panel nodes : 281 / Current airfoil cannot be set. and still returned a solution
  • The nine real contours above now raise ArgumentError naming the crossing panels, all nine in one process where each killer previously needed its own; the valid one still runs
  • test/solver/test_backend_comparison.jl PASS 15/15 (42.5 s) — the suite's only XFoil march · test/airfoil_aero/test_live_polar.jl PASS 79/79
  • Docs build clean (exit 0, no missing docstring) · up to date with origin/main at 23f129b
  • Full local suite: not run · GitHub CI: pending
  • REUSE lint: n/a, this repo has no bin/reuse_lint
  • Benchmark: n/a
  • Risk: the guard rejects contours that today happen to return — four of the nine above. Their answers were a panel solution on a curve that crosses itself, so I judge that no loss, but it is a behaviour change for any caller that was using one.

Scope

+96 / -1 across 5 files: the check and its two geometry helpers in airfoil_aero/, one regression testset, one changelog entry under ## Unreleased, four @docs entries. The regression test uses a synthetic folded Kulfan section rather than the mesh section that found this — that geometry is in a private repo and this one is public. Closes #322.

From 1-Bart-1/BeyondTheSim.jl#53 · task BeyondTheSim.jl-53

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018rHHzpuPmi8pBjVk3r5QZD
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@1-Bart-1

Copy link
Copy Markdown
Member

Merge main into this branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

analyze_sweep on an XFoilSolver can end the Julia process, exit code 0 and no exception

2 participants