Skip to content

[BUG] Fix aeroelastic strip ordering#190

Draft
camUrban wants to merge 4 commits into
mainfrom
bug/aeroelastic_logic
Draft

[BUG] Fix aeroelastic strip ordering#190
camUrban wants to merge 4 commits into
mainfrom
bug/aeroelastic_logic

Conversation

@camUrban

Copy link
Copy Markdown
Owner

Description

This pull request fixes two indexing errors in the structural side of the aeroelastic UVLM, both caused by incorrect assumptions about how the solver's panels are ordered relative to spanwise strips. The first fix rebuilds the strip leading edge point (SLEP) bookkeeping in AeroelasticUnsteadyRingVortexLatticeMethodSolver, which assumed the flat panel stack was grouped into one spanwise strip per WingCrossSection with a single chordwise row, when the stack is actually ordered chord-major within each wing. The second fix teaches the structural solve in AeroelasticUnsteadyProblem that a mirror-meshed Wing's panel grid runs tip to root spanwise, while its wing_cross_sections list, the structural solve, and the movement classes that consume the solve's output all run root to tip. Both fixes are internal to the aeroelastic coupling: no public interfaces change, and the purely aerodynamic solvers and the free flight coupling are unaffected.

Motivation

The SLEP misindexing meant that every panel behind the first chordwise row was matched to the wrong strip's leading edge point, so the per-strip aerodynamic moments that force each strip's torsional spring-mass-damper model were taken about the wrong points. The mirror-mesh ordering bug affected Wings meshed as reflections (those with mirror_only set to True, which covers symmetry types 2 and 3 as well as the reflected half that Airplane.process_wing_symmetry() produces when handling a type 5 symmetric Wing): each strip's deflection ODE was forced by its own aerodynamic moments but given the chord, strip width, and flapping moment arm of the strip mirrored across the semispan, and the resulting twist rows were applied to the WingCrossSections in reversed spanwise order. The visible symptom was that a geometrically and kinematically symmetric flapping configuration deformed asymmetrically. Both are deterministic logic errors rather than numerical artifacts, so no amount of mesh or time step refinement could have converged them away.

Relevant Issues

None.

Changes

  • In AeroelasticUnsteadyRingVortexLatticeMethodSolver.__init__(), rebuilt the slep_point_indices array to hold one entry per panel: the flat index of the panel in the first chordwise row at the same wing and spanwise position, whose front-left point is the strip's leading edge point. The old construction recorded one index per WingCrossSection and advanced the running panel count by spanwise panel counts alone, which assumed a spanwise-strip grouping that does not match the chord-major panel stack.
  • In AeroelasticUnsteadyRingVortexLatticeMethodSolver._update_bound_vortex_positions_relative_to_slep_points(), replaced the np.searchsorted() lookup and per-panel list comprehension with a single fancy-index gather through the per-panel slep_point_indices, which is both correct for the chord-major order and simpler.
  • In AeroelasticUnsteadyProblem.calculate_wing_deformation(), flipped the spanwise axes of the per-panel mass_matrix, aeroMoments_GP1_Slep, and inertial_moments arrays into root-to-tip strip order before the structural solve when the Wing is mirror-meshed.
  • In AeroelasticUnsteadyProblem.calculate_spring_moments(), mapped the root-to-tip spanwise section index to the panel grid's tip-to-root order when reading the strip width from frontLeg_G for mirror-meshed Wings.
  • Updated the docstrings of _apply_moment_updates() and calculate_spring_moments() to state that the deformation, angular velocity, mass, and moment arrays use root-to-tip spanwise order, and updated the block comments around the SLEP construction to describe the chord-major panel stack.

Dependency Updates

None.

Change Magnitude

Minor: Small change such as a bug fix, small enhancement, or documentation update.

Checklist (check each item when completed or not applicable)

  • I am familiar with the current contribution guidelines.
  • PR description links all relevant issues and follows this template.
  • My branch is based on main and is up to date with the upstream main branch.
  • All calculations use S.I. units.
  • Code is formatted with black (line length = 88).
  • Code is well documented with block comments where appropriate.
  • Any external code, algorithms, or equations used have been cited in comments or docstrings.
  • All new modules, classes, functions, and methods have docstrings in reStructuredText format, and are formatted using docformatter (--in-place --black). See the style guide for type hints and docstrings for more details.
  • All new classes, functions, and methods in the pterasoftware package use type hints. See the style guide for type hints and docstrings for more details.
  • If any major functionality was added or significantly changed, I have added or updated tests in the tests package.
  • Code locally passes all tests in the tests package.
  • This PR passes the ReadTheDocs build check (this runs automatically with the other workflows).
  • This PR passes the ascii-only, black, codespell, docformatter, isort, and pre-commit-hooks GitHub actions.
  • This PR passes the mypy GitHub action.
  • This PR passes all the tests GitHub actions.

camUrban added 2 commits June 11, 2026 14:29
The previous construction recorded one index per wing cross section and
only advanced the running panel count by each section's spanwise panel
count, which assumed the solver's flat panel stack was grouped into
spanwise strips with a single chordwise row. The stack is actually
ordered chord-major within each wing, so the searchsorted lookup
matched most panels to the wrong strip leading edge points, and the
moments taken about those points were wrong.

Record one index per panel that points directly at the panel in the
first chordwise row at the same wing and spanwise position. This also
lets the transform gather the leading edge points with a single fancy
index instead of the searchsorted lookup.
A mirror-meshed Wing's panel grid runs tip to root spanwise, while its
wing_cross_sections list runs root to tip. The structural solve assumed
every wing's spanwise panel index ran root to tip, so on the reflected
half of a type-5 symmetric wing, each strip's ODE was forced by its own
aerodynamic moments but given the chord, strip width, and flapping
moment arm of the strip mirrored across the semispan, and the resulting
twist rows were applied to the WingCrossSections in reversed spanwise
order. A symmetric flapping configuration therefore deformed
asymmetrically.

Flip the spanwise axes of the per-panel mass and moment arrays into
root-to-tip strip order before the structural solve, and map the
spanwise index when reading panel geometry, so the solve and the
movement classes that consume its output share one ordering for every
wing.
@camUrban camUrban added the bug Something isn't working label Jun 11, 2026
@camUrban camUrban self-assigned this Jun 11, 2026
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.15%. Comparing base (200ec76) to head (6a71d54).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #190   +/-   ##
=======================================
  Coverage   92.15%   92.15%           
=======================================
  Files          44       44           
  Lines        7822     7829    +7     
=======================================
+ Hits         7208     7215    +7     
  Misses        614      614           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@camUrban

Copy link
Copy Markdown
Owner Author

Hey @JonahJ27! While verifying these two fixes, I think I found a third problem in problems.py: a degrees/radians inconsistency across the structural coupling. Could you check my reasoning before we touch it?

The unit chain I see is:

  1. The spring-damper ODE works in SI radians: spring_numerical_ode documents k as Nm/rad and theta0 as radians, the aerodynamic forcing comes from the solver's moments in genuine N*m, and the sectional inertias are genuine kg*m^2. So the twist angles it returns are radians.
  2. Those angles are handed through calculate_wing_deformation and generate_airplane_at_time_step to AeroelasticWingCrossSectionMovement.generate_wing_cross_section_at_time_step without conversion, but that method documents deformation_angles_ixyz as degrees and adds them to the degree-valued angles_Wcsp_to_Wcs_ixyz. If that's right, the geometric twist we actually apply is a factor of 180/pi (~57x) weaker that it should be.
  3. Separately, generate_inertial_torque_function converts the flap phase with np.deg2rad but uses ampAngles_Gs_to_Wn_ixyz raw, so the sine-spacing inertial forcing looks about 57x too strong.
  4. The custom-callable branch also applies no amplitude scaling, although the spacingAnglesSecondDerivative_Gs_to_Wn_ixyz docstring says the derivative is supplied before amplitude scaling.

I don't think this can be fixed by re-documenting units alone, since the aero moments and the inertias are pinned to SI. The fix that matches the docstrings as written is to keep the structural model in radians and convert at the boundaries.

Heads up that if this is real, fixing it could change results substantially so we may need to retune the placeholder spring and damping constants in the examples/tests.

@camUrban camUrban added this to the v5.1.0 milestone Jun 11, 2026
@JonahJ27

Copy link
Copy Markdown
Contributor

Hi Cam,

Great catches here. I'm quite confident the symmetry bugs you caught were introduced in my potentially too hasty transition to running deformation on all of the wings. The numbers actually look much closer to what I had before the change (slightly different because of that other bug fix I put in in the :1 vs :2 indexing, but I digress). So your changes generally feel correct.

As for the degrees and radians stuff, I've implemented a fix for it, the notable part of it is that for the inertial and spring part, the amplitude we're using was also not getting converted out of degrees, so we convert and convert back as you suggested if I understand your point correctly. This leads to essentially the same outcome with the exception of aero torque which means that the outcome is exactly the same as before if I set aero scaling to 1/57.3. Alternatively, I can crank up the damping parameters significantly to damp out the aero moments, but that also requires changing the wing density significantly and makes the spring constant not matter unless it's very large. I would love a chance to chat about what is the best potential solution to this, and what is causing this aero moment to dominate the scale so much

@JonahJ27

Copy link
Copy Markdown
Contributor

I also can't push my changes to this branch so I'm making a new branch

@JonahJ27 JonahJ27 mentioned this pull request Jun 14, 2026
15 tasks
@JonahJ27

Copy link
Copy Markdown
Contributor

I've made some default parameter updates so that everything works without aero scaling

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants