Repository navigation
Add the attached-trailed vortex force as an opt-in solver setting - #389
Conversation
Kutta-Joukowski on each panel's two chordwise bound-to-trailing-edge filaments, with the total velocity at their three-quarter-chord point, behind is_with_attached_trailed_force (default false). solve! and calculate_results now assemble each panel's force and moment through one panel_force_moment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1-Bort-1
left a comment
There was a problem hiding this comment.
Independent review (advisory)
Verdict: APPROVE WITH COMMENTS · 2 inline, 0 off the diff
Good
- The flag is threaded everywhere
is_with_viscous_drag_correctionis: settings,solver_kwargs, the Solver field,solve,calculate_resultsandmake_dual_shadow. A grep for the viscous flag turns up no other place it would need to go. panel_force_momentreplaces the two copies of the force/moment assembly incalc_forces!andcalculate_results. This is the one-codepath rule applied at the change, and the card names it.moment_distnow readsloads.force, which is exactly whatforceheld before, so the section moment is unchanged. This matches the card's claim that the 2D distributions stay section quantities.- The
va_ref_unitfix is real: the old code kept it inwork_vectors[1], which the filament kernels (filament.jl:84,206) overwrite, and the new helper now calls those kernels inside the loop. It is named in the card. induced_velocitymakes the samecalculate_velocity_induced_single_ring_semiinfinite!call as the AIC loop and adds no second Biot–Savart path.- The tests pin the behaviour per panel: the force equals the off-run plus
attached_trailed_loads,solveagrees withsolve!,calc_forces!allocates nothing, and ForwardDiff matches FiniteDiff. The card also reports a red-before run with the force zeroed. - The card writes down the surprise with streamwise sections (+1.2 % instead of the paper's drop) instead of burying it.
Not good
src/body_aerodynamics.jl:622— The segment velocity usespanel.va_vec, sampled at the control point, rather than recomputing the inflow withomegaat the ¾-chord point. Kites almost always fly with nonzeroomega, so the case the card calls exact (uniform inflow) is the rare one in practice, and the extra error only shows up with the flag on.src/body_aerodynamics.jl:621— The unlabelled(2 / 3)is what places the sample at x/c = 0.75. Only the docstring ties it to the three-quarter-chord point, and changing the bound-point convention would silently move it. A named local or a derivation from the chord fractions would make the choice readable.induced_velocityreads each panel'swake.va/wake.direction, but the AIC loop passes one area-weightedva/va_unit. The two only agree as long aswake.jlgives every wake the same values, and that coupling is not stated anywhere.- The name
induced_velocityis very generic for a module-level helper, andcalculate_relative_alpha_and_velocity(panel, induced_velocity)already uses it as an argument name. Something likeinduced_velocity_atorhorseshoe_induced_velocitywould avoid the shadowing. - The sample point lies on the panel's own trailed filament and on its neighbour's coincident one. It relies on the core-radius desingularisation giving zero self-induction, and no test checks that.
- The O(N²) cost runs again on every
linearizeevaluation with Duals. The card's 17× slowdown comes from one@timerun, so it is only an estimate. - The Oswald assertion
oswald_on > oswald_offchecks only a direction, and the card itself says the size (+0.9 % against the paper's ~+13 %) does not match. Passing it confirms little. - The
CHANGELOG.mdentry repeats a benchmark number ('about 2 %') that belongs in the PR and will go stale when the test wing changes.
claude, rubric CLEAN_CODE.md. A different lab from the implementer
on purpose: a reviewer sharing its blind spots would not flag its mistakes.
| for (bound_point, te_point, orientation) in segments | ||
| chordwise = SVector{3, T}(te_point) - SVector{3, T}(bound_point) | ||
| point = SVector{3, T}(bound_point) + (2 / 3) * chordwise | ||
| velocity = SVector{3, T}(panel.va_vec) + |
There was a problem hiding this comment.
MINOR: The segment velocity uses panel.va_vec, sampled at the control point, rather than recomputing the inflow with omega at the ¾-chord point. Kites almost always fly with nonzero omega, so the case the card calls exact (uniform inflow) is the rare one in practice, and the extra error only shows up with the flag on.
There was a problem hiding this comment.
Fixed in 675f0e7: the inflow is now va_vec − ω × (sample point − control point), which is the inflow set_va! would give at the sample point itself.
| (panel.bound_point_2, panel.TE_point_2, -1)) | ||
| for (bound_point, te_point, orientation) in segments | ||
| chordwise = SVector{3, T}(te_point) - SVector{3, T}(bound_point) | ||
| point = SVector{3, T}(bound_point) + (2 / 3) * chordwise |
There was a problem hiding this comment.
MINOR: The unlabelled (2 / 3) is what places the sample at x/c = 0.75. Only the docstring ties it to the three-quarter-chord point, and changing the bound-point convention would silently move it. A named local or a derivation from the chord fractions would make the choice readable.
There was a problem hiding this comment.
Fixed in 675f0e7: named local three_quarter_chord = (0.75 − 0.25) / (1 − 0.25).
Name the three-quarter-chord fraction, rename induced_velocity to induced_velocity_at, and drop the benchmark number from the changelog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Local full suite: PASS (12 min, Julia 1.13.0, one cell of the matrix) |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
3d polar figs with before/after in pr body please |
|
Before/after 3D polars for the paper's swept wing and the V3 kite are now in the #389 description. |
TL;DR
A new setting,
is_with_attached_trailed_force(defaultfalse), adds a Kutta–Joukowski force on each panel's two chordwise filaments, the ones from the quarter-chord bound points to the trailing edge. The force uses the total 3D velocity at each filament's three-quarter-chord point (Gaunaa, Li & Pirrung, TORQUE 2026, §3). Those filaments already carried circulation and induced velocity but took no force, and that force is the swept-wing term the paper reports as missing from lifting-line codes.3D polars, before → after
The before curve is main at the merge base (1edac52's
src/, loaded as its own module in the same session). The after curve is this branch withis_with_attached_trailed_force = true. Same geometry, inputs, solver settings and α samples on both. With the flag left off, the branch reproduces main tomax|ΔCL| = 0,max|ΔCD| = 3e-17on both wings, so the default is unchanged.On the paper's swept + anhedral wing (VSM,
INVISCID, 60 panels, α −4…20°), the term moves CL down across the whole sweep: −2.18 % at α = 10° (0.6473 → 0.6332) and −0.97 % at 20°. CD falls more, −5.1 % at 10°, so the drag polar hardly moves. That is the direction the paper reports.The V3 kite (
vsm_settings.yaml, VSM, α −5…25°) goes the other way: CL rises by +3.4 % at α = 10° (0.8932 → 0.9235) and +4.3 % at 20°. CD rises by +5.2 % and +5.8 %, so CL/CD at 10° drops from 8.95 to 8.80. Below 5° the two curves coincide. This matches the streamwise-chord probe below. The V3's chords are streamwise (chord·x̂ ≥ 0.9995 on every panel sampled), which puts them 2–9° off the crossflow direction mid-span and 63° off at the tip panel. So the AT segments pick up sidewash instead of the paper's crossflow correction. Nothing in either figure lets me say whether the V3 result is closer to the CFD or the wind tunnel. The stall region, where it moves most, is outside what a lifting-line model resolves anyway.What the term does
Each segment gets
ρ · V_3D × Γ · (TE − bound point).V_3Dis the panel inflow, turned byomegaaboutreference_pointat the sample point itself, plus the velocity induced by every horseshoe and its wake. It is sampled two thirds of the way from the bound point to the TE (x/c = 0.75). This is the total velocity, notV⊥or the 2D-corrected one. Each panel keeps its own two segments with its own Γ, so the two coincident segments at a shared junction sum toρ Δa V × (Γ_i − Γ_{i+1}). The paper doesn't say how to attribute that junction force, and splitting it this way gives the same wing total with no extra bookkeeping. Each segment force acts at its sample point for the moment aboutreference_point.moment_distandlift_dist/drag_diststay 2D section quantities; onlyf_body_3D/m_body_3Dand everything summed from them carry the term.It runs in
solve!, insolveand, through the Dual shadow, inlinearize. The circulation is untouched, because the force is assembled after γ converges.Result on the paper's §5.2 case
The paper gives only the tip offsets (10 % back, 25 % down) and CL_VLM = 0.753 at α = 10°, which implies AR ≈ 4. The exact planform is in its online appendix. The test wing is AR 4 with parabolic offsets, 60 panels,
INVISCID(Cl = 2πα). Change in CL from turning the force on:The blade-force Oswald factor rises, by +0.9 % (VSM) and +3.8 % (LLT), well short of the paper's ~+13 %. That gap is expected: the 2D inner model here is not LL-Gaunaa's (#317, #318), so only the lift shift is asserted.
Where I'd push back
The term only appears when sections are perpendicular to the quarter-chord line, which is the paper's Crossflow Principle (p. 3). My first probe used streamwise chords on the same swept + anhedral wing. There the AT segments lie along the freestream, and CL went +1.2 % instead of down, driven by sidewash alone. The sign convention checks out: Kutta–Joukowski on the bound filament with the same convention reproduces the panel lift. So the force follows whatever chord direction the geometry defines, and a streamwise-sectioned swept wing will not get the paper's correction. The V3 kite in
data/is one of those, as the second figure above shows. Getting the paper's correction on it would need crossflow sections in the geometry, which is a separate change.Also in the diff
solve!andcalculate_resultseach assembled a panel's force and moment frompanel_loadsin their own copy. Both now call onepanel_force_moment, which is where the new term lives.calculate_resultskeptva_ref_unitinwork_vectors[1], which the Biot–Savart kernels overwrite. It is anSVectornow, so calling the kernels inside that loop can no longer corrupt the drag projection.On the review
induced_velocity_atreads each wake's ownva/direction, which is the wake the filaments actually hold. The AIC loop passes the shared area-weighted vector thatfrozen_wake!also writes into every wake, so the two agree by construction today. If they ever diverge, the AIC loop is the one to change, not this helper.test/filament/test_bound_filament.jl("Point Exactly on Filament").@timerun; the term is opt-in, and a §5 benchmark belongs with whoever turns it on by default.Verification
test/solver/test_attached_trailed_force.jlgoes red with the force zeroed inattached_trailed_loads:49 passed, 4 failed, the CL-drop and Oswald-rise assertions for VSM and LLT. Green after: 53/53 (juliaserver).test_attached_trailed_force,test_solver(zero-alloccalc_forces!),test_viscous_drag_correction,test_body_aerodynamics,test_forwarddiff,test_verification,test_settings,test_backend_comparison.calc_forces!allocates 0 bytes,solveandsolve!agree to 0.0, and the ForwardDiff Jacobian matches FiniteDiff to < 1e-3.test_attached_trailed_force53/53,test_forwarddiff10/10,test_solver50/50,test_body_aerodynamics4909/4909 (juliaserver).agent ci-local, Julia 1.13, on 675f0e7): PASS (8 min) · GitHub CI on 675f0e7: PASS (1.12 macOS/ubuntu/windows, 1.13 ubuntu, docs, setup).max|ΔCL| = 0,max|ΔCD| = 3e-17over both α sweeps (juliaserver,generate_polar_data).private_functions.md.calc_forces!at 60 panels takes 0.62 ms on vs 0.036 ms off, a single@timeeach and not a §5 benchmark. The term is O(N²), two induction sweeps per panel, which is why it is opt-in.omegaterm at the sample point has no test of its own. The tests run withomega = 0, and onlyset_va!(body_aero, va_vec, omega)sets a nonzero one (the distributedset_va!zeroes it).Scope
+211 / −14 across 7 files.
src/body_aerodynamics.jlholds the three helpers (induced_velocity_at,attached_trailed_loads,panel_force_moment) and theva_ref_unitfix.settings.jl/solver.jlthread the flag the wayis_with_viscous_drag_correctionis threaded, includingmake_dual_shadow. The new test file is 106 lines.Opened by
1-Bort-1, an AI agent working for @1-Bart-1.Closes #319 · task
VortexStepMethod.jl-319