Repository navigation
Quarter-chord force law as xp kernels + public API; allocation-free filament kernels - #166
Merged
Merged
Conversation
The induced-velocity kernels carried vectors as length-3 arrays; in numba every temporary (np.cross, r1 - r0, np.zeros(3)) is a heap allocation, and those allocations were ~95% of the cost: ~1.1 us per filament evaluation. They now carry 3-tuples, which numba keeps on the stack (~15-19 ns). Formulas, branch structure and summation order are unchanged, so the matrices still match the Filament.py reference to round-off (the equivalence tests at rtol 1e-12 pass). error_model="numpy" keeps NumPy's inf/nan semantics for zero denominators instead of numba's scalar ZeroDivisionError, so degenerate geometry behaves as before. V3 kite, 36 panels: AIC 7.3 -> 0.10 ms; casadi_newton solve 44 -> 24 ms. Affects assemble_AIC_matrices, assemble_curved_bound_correction_AIC and induced_velocity_at_points. Numerical, not physical; outputs unchanged. Generated with Claude Code
The section force law moves into VSM.core.panel_kernels, pure functions over an xp namespace (numpy or casadi), so CasADi callers (AWETrim's CasADi trim) use VSM's formula instead of restating it. compute_results calls the same kernels for its per-panel force, so the formula lives once. - BodyAerodynamics.compute_quarter_chord_AIC_matrices(core_radius_fraction, va_norm_array, va_unit_array): public quarter-chord induction, AIC_LLT + the cached curved bound-vortex correction. update_effective_angle_of_ attack_if_VSM now uses it (one source). - panel_kernels: relative_flow, quarter_chord_velocity, direction_angle, section_force_directions, section_force_magnitudes, section_force_vectors, panel_forces, panel_moment_local, attached_trailed_forces, row helpers. compute_results and compute_attached_trailed_vortex_forces call them. - Solver.casadi_panel_force_function(): CasADi function (v_rel, v_qc, axes, chord, width, rho) -> per-panel force, local moment, alpha, alpha_dir, umag, cl, cd, cm, on the same polar lookups as the casadi_newton residual (factored into _casadi_polar_lookups; the residual function is unchanged). Refactor plus API: no physics change. Every numeric solve output matches VSM 1201f6f to <= 1.5e-14 relative for is_aoa_corrected True and False on a straight, a swept, an arc wing and the LEI V3 kite (frozen reference in tests/BodyAerodynamics/data, written with 1201f6f). CasADi kernels equal the NumPy ones to 1e-12; a CasADi graph with the induction matrices as parameters reproduces compute_results panel forces to 2e-16. Generated with Claude Code
Contributor
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing. Review the Copilot code review billing settings.
…dual calls - compute_trefftz_plane_induced_drag: the O(n^2) per-panel Python loop with np.cross becomes one vectorised evaluation (1.3 -> 0.2 ms at 36 panels). It is a far-wake diagnostic computed on every solve; output unchanged. - Wing.compute_projected_area: vectorised over the strips (1.3 -> 0.1 ms). Also stops normalising the caller's z_plane_vector in place. - gamma_loop_casadi_newton: the inputs that are constant within a solve are converted to CasADi matrices once, and the loop calls a cached variant of the residual function with only the six outputs it reads (same graph). ~185 -> ~77 us per residual evaluation. Numerical, not physical. Sums are reordered, so outputs move at round-off only: every solve output still matches 1201f6f to <= 1.5e-14 relative. V3, 36 panels, casadi_newton: ~24 -> ~17 ms per solve. Generated with Claude Code
This was referenced Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two changes, both behaviour-preserving (no physics change):
xpkernels, so CasADi callers (AWETrim's CasADi trim) can use VSM's formula instead of restating it.compute_resultscalls the same kernels for its own per-panel force, so the formula lives in one place.casadi_newtonsolve about 1.8× faster.New public functions (for the AWETrim follow-up)
BodyAerodynamicsv_qc = va + AIC_qc @ gamma.update_effective_angle_of_attack_if_VSMnow uses it, so there is one source.VSM.core.panel_kernelsEvery function takes
xp=numpy(the default) orxp=casadi. Conventions:(n, 3)rows;(n,)vectors, which are(n, 1)columns in CasADi.alphawithq = 0.5 rho umag^2.alpha_dir:alpha_dir=Noneusesalphaitself: theis_aoa_corrected=Falselaw.direction_angle(quarter_chord_velocity(va, gamma, *AIC_qc))gives theTruelaw (TAT3).compute_results, as before.Solvercasadi_newtonresidual. Those lookups are factored into_casadi_polar_lookups; the residual function's inputs, outputs and graph are unchanged.v_qcrather than the matrices, so the graph stays O(n). The caller forms the dense product, the same design as the Newton residual takingv_rel.panel_kernels.attached_trailed_forces.What AWETrim should call
AIC_qc = body.compute_quarter_chord_AIC_matrices(core_radius_fraction, va_norm, va_unit);v_rel = panel_kernels.quarter_chord_velocity(va, gamma, *AIC_cp, xp=ca);v_qc = panel_kernels.quarter_chord_velocity(va, gamma, *AIC_qc, xp=ca)foris_aoa_corrected=True, orv_qc = v_relforFalse.force, moment_local, ... = solver.casadi_panel_force_function()(v_rel, v_qc, x_airf, y_airf, z_airf, chord, width, rho).panel_kernels.attached_trailed_forces(...). Itsxpconventions match AWETrim'spanel_kernels.py.The row helpers and
relative_flow/panel_forces/attached_trailed_forceskeep the names and conventions of AWETrim'spanel_kernels.py. Two differences:panel_forceshas nospanwise_directionargument (it was unused there) and takes an optionalalpha_dir.panel_forceskeeps VSM's operation order (cm 0.5 rho U^2 c^2) so it matchescompute_resultsexactly.No-change regression
Reference.
tests/BodyAerodynamics/data/panel_kernel_reference.npzholds every numeric output ofSolver.solve(576 arrays), written with VSM at1201f6f, the original code before both commits. It covers six cases, each withis_aoa_correctedTrue and False:Largest deviation from
1201f6f, relative to the quantity's own scale, or to the case's force/moment scale for entries that are zero by symmetry:TrueFalseOther checks:
DMinputs) equal the NumPy kernels to 1e-12.gamma,vaand both sets of induction matrices as MX parameters reproduces thecompute_resultspanel forces at a convergedgamma: force 2e-16, local moment 8e-15, for both laws.Note on the CasADi comparison.
solvepassescompute_resultsthealpha/Umagof the circulation loop's last iterate. Those lag the returnedgammaby one iteration, so they agree with it only toallowed_error. The graph test therefore compares againstcompute_resultsevaluated at one consistentgamma. This is existing behaviour and is not changed here. A CasADi trim that evaluates the force at its owngammawill differ fromsolve()by O(allowed_error).Filament kernels (
utils.py)Cause. The numba kernels carried vectors as length-3 arrays. Every temporary (
np.cross,r1 - r0,np.zeros(3)) was a heap allocation, and those allocations were about 95 % of the cost.Change. The kernels now carry 3-tuples, which numba keeps on the stack. The formulas, branches and summation order are unchanged.
error_model="numpy"keeps NumPy's inf/nan semantics for zero denominators, where numba's default would raiseZeroDivisionErroron scalars, so degenerate geometry behaves as before. The existing equivalence tests againstFilament.py(rtol 1e-12) pass.casadi_newtonsolve, 36 panelsThis also speeds up
assemble_curved_bound_correction_AICandinduced_velocity_at_points(the attached-trailed force).Post-processing and Newton-call overhead (third commit)
compute_trefftz_plane_induced_dragis a far-wake diagnostic computed on every solve. Its O(n²) Python loop is now vectorised: 1.3 → 0.2 ms at 36 panels.Wing.compute_projected_areais vectorised: 1.3 → 0.1 ms. It also no longer normalises the caller'sz_plane_vectorin place.gamma_loop_casadi_newtonconverts the inputs that are constant within a solve to CasADi matrices once, and calls a cached six-output variant of the residual function (same graph): ~185 → ~77 µs per evaluation.Outputs move at round-off only (≤ 1.5e-14 against
1201f6f). The test counts are unchanged.V3, 36 panels, best of 7 sweeps (machine under other load),
casadi_newton: 50.7 ms at1201f6f→ 23.8 ms after commit 2 → 17.0 ms after commit 3.Tests
Full suite: 201 passed, 5 skipped. That is the 183 passed, 5 skipped at
1201f6fplus 18 new tests intests/BodyAerodynamics/test_panel_kernels.py.Written by Claude, reviewed by Oriol Cayon