Conversation
Derivatives of the force and moment coefficients with respect to angle of attack and sideslip, by the chain rule through linearize's va columns, and a trim-angle search on CMy with the slope from those derivatives. The inflow formula moves out of set_va!(body_aero, settings) into apparent_wind so both share it. Refs #330 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 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 derivatives are built on
linearize, as the card promises:va_idxs=1:3withtheta_idxs=nothing, andomegaread frombody_aero.omegaatsrc/solver.jl:1319-1321, so there is no second finite-difference path. - The formula in
apparent_windmatches the removed body ofset_va!(settings)term for term, and that method now calls it, so the inflow has one source. dva/dalphaanddva/dbetacome from ForwardDiff overapparent_wind, so the formula's derivative is not written out a second time.- The docstring fix fits the geometry: the test wing runs from the leading edge at x=0 to the trailing edge at x=1, and
docs/src/settings.md:54has z = up, so the old forward/down labels were wrong. - The tests name what they protect, check both slope signs and a range with no trim, and compare against central differences of
solve!rather than againstlinearizeitself. - Public symbols are in
functions.md, private helpers inprivate_functions.md, and CHANGELOG and export and include lines are added, matching the plan in the card.
Not good
src/stability.jl:46—kwargsreachlinearizeand sosolve!for the slope, but not theCMysweep inpitch_moment_coeff. A caller passingreference_point=(asolve!keyword) gets trims found aboutsolver.reference_pointbut slopes about another point, so the stable/unstable verdict can be silently wrong.src/stability.jl:61—pitch_moment_coeffnever checkssolver.lr.converged, and line 48 throws awayderivatives.converged. A trim bisected on unconvergedCMyvalues comes back looking just like a good one.coeffs_atin the test ispitch_moment_coeffreturning all six coefficients; one private src helper returning all six, withtrim_angletaking[5], would serve both.- The logic of the trim predicate sits in the closure
is_nose_down(§6); a named helper, orpitch_moment_coeff(...) < 0passed straight in, reads the same without it. - The trim tests use the default
Solver, whereuse_gamma_previs on, so each bisection step starts from the last step's gamma; the derivative test turns it off, and the trim tests do not say why they leave it on. - The card says to pass
backend=nothingfor aNONLINsolver, but no test covers that path throughstability_derivativesortrim_angle. trimsis fixed toFloat64, so aFloat32or dualalpha_rangeis converted without warning; fine for now, but worth a line in the docstring if it is intended.
claude, rubric CLEAN_CODE.md. A different lab from the implementer
on purpose: a reviewer sharing its blind spots would not flag its mistakes.
| nose_down[i] == nose_down[i+1] && continue | ||
| alpha = bisect_sign_change(is_nose_down, alpha_range[i], alpha_range[i+1], | ||
| alpha_tol) | ||
| derivatives = stability_derivatives(solver, body_aero, alpha, beta, wind_speed; |
There was a problem hiding this comment.
MINOR: kwargs reach linearize and so solve! for the slope, but not the CMy sweep in pitch_moment_coeff. A caller passing reference_point= (a solve! keyword) gets trims found about solver.reference_point but slopes about another point, so the stable/unstable verdict can be silently wrong.
There was a problem hiding this comment.
Fixed in b50a2b5: trim_angle takes an explicit backend instead of splatting kwargs, so the sweep, the bisection and the slope all take moments about solver.reference_point.
| """ | ||
| function pitch_moment_coeff(solver, body_aero, alpha, beta, wind_speed) | ||
| set_va!(body_aero, apparent_wind(alpha, beta, wind_speed), body_aero.omega) | ||
| return solve!(solver, body_aero).moment_coeffs[2] |
There was a problem hiding this comment.
MINOR: pitch_moment_coeff never checks solver.lr.converged, and line 48 throws away derivatives.converged. A trim bisected on unconverged CMy values comes back looking just like a good one.
There was a problem hiding this comment.
Fixed in b50a2b5: the sweep, bisection and slope solves all run with throw_on_fail, so an unconverged solve throws SolveFailure instead of returning a trim (new testset, red on the old code: max_iterations=2 returned a trim at 1.00° with slope -1.56).
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…-and Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Local full suite: PASS (8 min, Julia 1.12.7, one cell of the matrix) |
…-and Resolves set_va!(body_aero, settings) against the va rename (#349): the body calls apparent_wind and names the vector va_vec, as does stability_derivatives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CMy sweep and bisection solve with throw_on_fail, as does the slope's linearize, so a trim is never bisected on unconverged moments. trim_angle takes an explicit backend instead of splatting kwargs into linearize, which let a reference_point reach the slope but not the sweep. coeffs_at_angles replaces pitch_moment_coeff and the test's copy of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TL;DR
New
stability_derivatives(solver, body_aero, alpha, beta, wind_speed)returns the six force and moment coefficients and their derivatives with respect to angle of attack and sideslip. Newtrim_angle(solver, body_aero, beta, wind_speed)finds whereCMychanges sign and returns the slopedCMy/dalphaat each trim, so you can see whether it is stable. The Python package has both and this package had neither. Both are built onlinearize, as #330 proposes, rather than on a second finite-difference path.How it works
linearize(...; va_idxs=1:3, aero_coeffs=true)gives the coefficient Jacobian with respect tova. That is multiplied bydva/dalphaanddva/dbeta, which ForwardDiff takes fromapparent_wind. Moments are aboutsolver.reference_point, the rotation rate is whateverbody_aero.omegaholds, and extra keywords go tolinearize(e.g.backend=nothingfor aNONLINsolver, since ForwardDiff only runs onLOOP).apparent_wind(alpha, beta, wind_speed)is the formulaset_va!(body_aero, settings)already used,V·[cosα cosβ, sinβ, sinα cosβ], moved out so both callers share it. The old docstring labelled those components forward/right/down; the body frame is x back, z up, so the docstring now just names the formula.alpha_range(default −5° to 15° in 2° steps, in radians), bisects every bracket whereCMy < 0flips toalpha_tol(default 1e-5 rad), and takes each slope fromstability_derivatives. It returns every trim found, and an empty vector if there is none.throw_on_fail: the sweep, the bisection and the slope'slinearize. A solve that misses the solver's tolerances throwsSolveFailureinstead of steering the bisection. On the test wing withmax_iterations=2, the version before the review silently returned a trim at 1.00° with slope −1.56; converged, the answer is 1.666° and −1.11.trim_angletakesbackendexplicitly rather than passing keywords through tolinearize. With pass-through, areference_point=reached the slope but not theCMysweep, so the trim and its stability verdict could be about two different points. Both review comments are addressed in b50a2b5.Where this differs from the Python package
cos βon z, asset_va!(settings)has it. Python main usessin αwithout it, so the numbers differ from Python at nonzero sideslip.Split
#330 holds more than one idea, so this is part 1:
linearize'somegacolumns rotate about the origin while moments are taken aboutsolver.reference_point, so porting the rates now would mean a second copy ofω × (r − r0).linearizeandmake_dual_shadowtake more than one wing. Until it lands,stability_derivativesthrows on a multi-wing body, and so doestrim_angleonce it has found a trim.Found on the way (not changed)
examples/billowing.jlhas the same formula.examples/linearize_check.jlleavescos βoff z.apparent_windis private, so I left the examples alone..github/workflows/CI.ymlsetsfail-fast: false. Changing that is its owncleanup:PR.src/,test/,examples/anddocs/for stability, derivative, trim, malz and bisect. Nothing to reuse; the one bisection, inobj_slice.jl, carries its own per-step payload.Verification
test/solver/test_stability.jl, red before, green after.includegave0 passed, 0 failed, 1 erroredbecausestability_derivatives,trim_angleandapparent_windwere undefined.a solve that misses the tolerances throwstestset was red on the code before b50a2b5: it returned a trim instead of throwing.a NONLIN solver with backend=nothing finds the same trimtestset covers the finite-difference path. It matches theLOOP+ ForwardDiff trim to 1e-4.solve!(step 1e-4 rad) to about 1e-8 relative, e.g.dCFz/dα4.414345384 against 4.414345369. Tested to rtol 1e-4.cm = 0.05and moments about the leading edge, it finds one trim at 1.666° with slope −1.11 /rad. Withcm = −0.05and moments about the trailing edge, one trim at −0.050° with slope +3.34 /rad.test/body_aerodynamics/test_body_aerodynamics.jl: green, includingset_va! with VSMSettings(5/5), which covers the moved formula.CHANGELOG.md; both are kept.test_viscous_drag_correction.jlpassed 90/90 on that merge.varename): the conflict was the body ofset_va!(body_aero, settings). It now callsapparent_wind, and the vector local isva_vec, as instability_derivatives.test_body_aerodynamics.jlis green on the merged tree.jetlsis not installed on the box, so it was not run.dCFy/dβis exactly zero on a flat wing, so the side-force derivative is only checked against the finite difference where it is zero. The other five sideslip derivatives are nonzero and checked.Scope
+209 / −25 across 8 files:
src/stability.jl(92 lines, about half of them docstrings)test/solver/test_stability.jl(92 lines)src/body_aerodynamics.jl:apparent_windmoved out ofset_va!(settings), and its docstring cut from 25 lines to 3, net −12runtests.jl, the export list and the include listStack: 1/3 → #345 (rate derivatives, blocked on #329), #344 (multi-wing
linearize).Refs #330 · task
VortexStepMethod.jl-330