Skip to content

ENH: Refactor Aero Surface, Make GenericSurface the Mother Class - #1064

Open
MateusStano wants to merge 27 commits into
enh/eventsfrom
enh/aero-refactor
Open

MateusStano wants to merge 27 commits into
enh/eventsfrom
enh/aero-refactor

Conversation

@MateusStano

@MateusStano MateusStano commented Jul 11, 2026 •

Copy link
Copy Markdown
Member

Summary

All aerodynamic surfaces now come from one mother class, GenericSurface. Before, the nose cone, fins and tail each had their own way of computing forces. Now they all describe themselves with the same set of coefficients, and the flight treats every surface the same way.

I took the opportunity to rework how coefficients are stored, to add full-vehicle aerodynamics, and to redo the stability analysis on top of it.

Here is a summary of all the changes:

Aerodynamic surfaces (rocketpy/rocket/aero_surface/)

  • GenericSurface is now the mother class of every surface: NoseCone, all fins, Tail, RailButtons and AirBrakes.
  • _BarrowmanSurface: new private base for the surfaces that use Barrowman equations (nose, fins, tail).
  • AeroCoefficient: new class that holds one coefficient. It accepts a number, a function, a Function, a list of points or a CSV file, and only keeps the variables the coefficient really depends on.
  • ControllableGenericSurface: new surface whose coefficients also depend on control variables, such as a canard deflection. AirBrakes is now one of these.
  • AeroSurface is deprecated. It still works and isinstance(surface, AeroSurface) is still true.

New options on GenericSurface and LinearGenericSurface:

  • Coefficients in the body frame (cN, cY, cA) or the wind frame (cL, cQ, cD), chosen with force_convention.
  • Coefficients from a CSV file, including several coefficients from one file (from_csv).
  • Coefficients given against the total angle of attack (alpha_total).
  • Rate derivatives such as cm_q next to a tabulated coefficient, to add damping.
  • interpolation and extrapolation options for tabulated coefficients.
  • reynolds_length, to use a different length for the Reynolds number.
  • axisymmetric=True on LinearGenericSurface, to give only the pitch-plane derivatives.
  • active_during ("always", "power_on", "power_off") and active, to make a surface work only in part of the flight.
  • Every argument after name is keyword-only.

Rocket

  • add_full_body_aerodynamics: describe the whole rocket with one surface (from CFD, wind tunnel, OpenRocket...). overwrite=True removes the other surfaces and clears the drag curves.
  • to_coefficients and to_surface: turn an assembled rocket into one set of coefficients, or one surface.
  • power_on_drag and power_off_drag can now depend on more than Mach (angle of attack, Reynolds, rotation rates).
  • Stability now has pitch and yaw versions: aerodynamic_center, center_of_pressure, neutral_point, static_margin, stability_margin and their _yaw twins.
  • stability_phase: choose if the stability numbers describe the powered or the coasting rocket.
  • disturbance_response: how the rocket oscillates after a disturbance.
  • is_axisymmetric, is_incidence_linear and length.

Flight

  • Yaw-plane stability margins (stability_margin_yaw, static_margin_yaw, min, max, out of rail).
  • Dynamic stability: pitch_natural_frequency, pitch_damping_ratio, their yaw versions, corrective_moment_coefficient, damping_moment_coefficient and disturbance_response.
  • New event commands activate_surface and deactivate_surface, to switch a surface on or off at any moment of the flight.

Function

  • Data on a regular grid is now found automatically and looked up much faster. Function.from_regular_grid_csv and interpolation="regular_grid" are deprecated.

Fixes

  • The equations of motion now take the moments about the rocket's center of mass. This was wrong in master for a rocket with propellant.
  • Drag now always acts against the air velocity, also when the rocket falls tail first.
  • Saving and loading a flight with air brakes or sensors works again: the controller is reconnected to the rocket's own objects.

Plots and prints

  • Aero surface, rocket and flight plots and prints updated for the new coefficients.
  • New stability plots in Flight.plots (stability_margin_data, stability_and_control_data, stability_summary) and Rocket.plots.stability_margin_yaw.

Docs

  • docs/user/rocket/generic_surface.rst rewritten: frames, coefficients, CSV files, damping, interpolation, activation, whole-vehicle aerodynamics.
  • New guide: docs/user/center_of_pressure_and_stability.rst.
  • New reference pages: ControllableGenericSurface, PointMassRocket, PointMassMotor, EmptyMotor, StochasticAirBrakes.

Notes for reviewers

The diff is large. The best way in is to read the two guides above, then aero_coefficient.py and generic_surface.py. The other surfaces are small once those two are clear.

Breaking change

  • No, but:
    • AeroSurface is deprecated in favor of GenericSurface.
    • Function.from_regular_grid_csv and interpolation="regular_grid" are deprecated.
    • Flight results change slightly because of the fixes above.

MateusStano and others added 8 commits June 22, 2026 21:55
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every aerodynamic surface is now rooted in GenericSurface and stores its
force coefficients in the body frame (cN/cY/cA) plus the cm/cn/cl moments,
exposing all nine coefficients (cL/cD/cQ/cN/cY/cA/cm/cn/cl) with the wind
trio lazily derived. A force_convention argument lets users supply wind- or
body-frame coefficients. Barrowman surfaces (nose, tail, fin sets) keep the
classic geometric normal-force/moment method, report the force at the
geometric center of pressure via the classic 180-degree surface rotation,
and expose cN_alpha/cY_beta stability slopes (the old clalpha relabelled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the poorly defined calisto_linear_generic fixture, which kept the
Barrowman nose cone and tail and swapped only the fins for a
LinearGenericSurface with arbitrary made-up coefficients. The new fixture is a
standalone Calisto whose nose cone, tail and fins are all LinearGenericSurfaces
built from coefficient curves extracted off the standard Barrowman surfaces
(normal-force-curve slope, center of pressure, fin roll damping).

Each linear surface applies its force at its own origin and is placed at the
source surface's center-of-pressure station, so both the static-margin path and
the flight-moment path land at the same point as calisto_robust. The resulting
flight matches the standard Calisto (identical apogee, out-of-rail time and
ascent angle of attack), so the fixture now exercises the linear
generic-surface path against a known-good reference.

Also fix test_linear_generic_surface_flight_is_stable to check the angle of
attack only during the ascent off the rail: on the rail the freestream speed is
~0 and the angle of attack is reported as a degenerate 90 degrees for any
launcher, which previously failed the < 45 assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MateusStano
MateusStano changed the base branch from master to enh/events July 11, 2026 13:12
@MateusStano MateusStano self-assigned this Jul 11, 2026
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.82979% with 87 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (enh/events@5fff32c). Learn more about missing BASE report.

Files with missing lines Patch % Lines
rocketpy/rocket/aero_surface/aero_coefficient.py 93.68% 12 Missing ⚠️
rocketpy/prints/aero_surface_prints.py 77.55% 11 Missing ⚠️
rocketpy/rocket/rocket.py 95.26% 10 Missing ⚠️
rocketpy/plots/aero_surface_plots.py 85.45% 8 Missing ⚠️
rocketpy/plots/flight_plots.py 90.69% 8 Missing ⚠️
rocketpy/rocket/aero_surface/generic_surface.py 95.58% 8 Missing ⚠️
rocketpy/plots/rocket_plots.py 87.50% 7 Missing ⚠️
rocketpy/prints/rocket_prints.py 64.28% 5 Missing ⚠️
rocketpy/simulation/helpers/flight_derivatives.py 79.16% 5 Missing ⚠️
rocketpy/prints/flight_prints.py 89.18% 4 Missing ⚠️
... and 4 more
Additional details and impacted files
@@              Coverage Diff              @@
##             enh/events    #1064   +/-   ##
=============================================
  Coverage              ?   81.88%           
=============================================
  Files                 ?      129           
  Lines                 ?    16880           
  Branches              ?        0           
=============================================
  Hits                  ?    13822           
  Misses                ?     3058           
  Partials              ?        0           

☔ 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.

Comment thread docs/user/rocket/generic_surface.rst
Comment thread docs/user/rocket/generic_surface.rst
Comment thread docs/user/rocket/generic_surface.rst
Comment thread docs/user/rocket/generic_surface.rst Outdated
Comment thread docs/user/rocket/generic_surface.rst Outdated
Comment thread docs/user/rocket/generic_surface.rst
Comment thread rocketpy/rocket/aero_surface/linear_generic_surface.py
Comment thread rocketpy/rocket/aero_surface/fins/_base_fin.py Outdated
@ting-hong-shieh

Copy link
Copy Markdown

I checked two behaviors from the upstream worklist on head 84d3b6a9f0b4a8074d764130dc3d189bdc402036.

Scalar coefficients

GenericSurface(1, 1, {"cN": 0.75, "cA": 0.2, "cm": -0.1}) constructs successfully. The three values evaluate unchanged and cN.depends_on == (). The focused initialization/constant selection reports 13 passed, 23 deselected.

Wind-to-body force mapping (#932)

At alpha = 0.2 rad, beta = 0.1 rad, the head produces these body vectors for unit wind-frame coefficients:

side: [ 0.995004165, -0.019833838, -0.097843395]
lift: [ 0.000000000, -0.980066578,  0.198669331]
drag: [-0.099833417, -0.197676812, -0.975170327]

Those match Rx(-alpha) @ Ry(+beta) applied to [Q, -L, -D]; current develop has the opposite signs in the affected components. I posted the complete before/after table on #932.

Both pieces appear covered by this draft, but the branch also includes a substantially larger aerodynamic redesign. If a smaller review unit would help, the scalar AeroCoefficient support and the #932 force-frame correction are independently testable candidates for extraction. I have not opened overlapping pull requests.

Environment: Python 3.12.6; NumPy 2.5.2; pytest 9.1.1; macOS 26.5.2 arm64. Current develop: cb6106a717207dd8fc2dfe1446d80ff75022f21b.

@Gui-FernandesBR

Copy link
Copy Markdown
Member

This is a huge PR, yet not finished tho

MateusStano and others added 11 commits September 19, 2026 12:19
Conflicts were resolved toward the event context design of enh/events:

- controller.py, rocket.py: the controller save/load fix from 3260573 is
  ported to the context convention. The controlled-object hashes are still
  saved so a loaded rocket reconnects its controllers, controller_needs is
  dropped since the context works values out when first read, and the
  legacy air brakes wrapper keeps the 7-argument form but passes the live
  context["controlled_objects"] instead of the captured air brakes.
- event.py, event_builders.py, event_calling.py, event_commands.py,
  flight_phase.py: docstrings follow the new signatures (no needs or
  kwargs), keeping the reworded text where the parameter still exists.
- tests/unit/simulation/test_flight.py: both import additions kept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Rocket.stability_margin takes (mach, time) again, at zero angle of attack
- aerodynamic center counts each surface's whole moment (pure couples, axial
  force at a sideways offset, canted individual fins)
- Rocket.disturbance_response and Flight.disturbance_response, built on
  attitude oscillator helpers shared with the flight's dynamic stability
- GenericSurface: data against alpha_total is split between the pitch and yaw
  planes from the variable name alone, also for lift and drag; force_convention
  is only "body" or "wind"
- LinearGenericSurface(axisymmetric=True) fills in the yaw-plane derivatives;
  add_full_body_aerodynamics no longer takes symmetric
- remove angular_position from GenericSurface
- stability guide: simpler center of pressure section, displayed C1 and C2
  equations, disturbance response section, parts 6 and 7 removed
- generic surface guide: full list of variable names, total angle of attack
  by variable name, axisymmetric linear surfaces
- rocket usage: drag curve at an angle of attack, stability_margin(mach, time)
A GenericSurface takes a rate derivative such as cm_q next to a coefficient,
to add damping to tabulated data: it is multiplied by its reduced rate and
added to the coefficient. Documented in the generic surface guide.
Brings in the attribute-style event context (`context.state.vz`,
`context.state_dot.az`, `context.controlled`) and the move of the event
code from `simulation/helpers` to `simulation/events`.

Conflict in `rocketpy/control/controller.py`: kept the `enh/events`
side, where `__build_controlled` replaces
`__verify_controlled_objects_name`.

Ported two places that merged cleanly but still used what that commit
removed:

- `_Controller.rebind_controlled_objects` now rebuilds
  `context.controlled` from the reconnected objects.
- The wrapper for legacy positional air brake controllers reads the
  air brakes from `context["controlled"][0]` instead of
  `context["controlled_objects"]`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A surface's `active_during` now accepts only "always", "power_on" and
"power_off". The function form `active_during(t, flight)` is removed:
to switch a surface at any other moment, an event calls one of two new
commands from its callback,

    context.event.commands.activate_surface(surface)
    context.event.commands.deactivate_surface(surface)

and the solver restarts at the switch. A surface that only appears
later in the flight is built with the new `active=False` argument, so
that it starts each flight switched off.

The flight stores each switch with its time, so `surface.is_active(t,
flight)` is still a plain question about time and the outputs computed
after the flight see the surfaces that were on at each moment. The
surfaces themselves are not modified, so another flight of the same
rocket starts from their own `active` setting.

The stability analysis leaves out a surface built with `active=False`.
… name

GenericSurface, LinearGenericSurface and ControllableGenericSurface now
take their shared arguments in the same order, with each class's own
argument last (`axisymmetric`, `controls`). ControllableGenericSurface
had `interpolation`/`extrapolation` and `force_convention`/
`active_during` swapped relative to the other two.

Every argument after `name` is keyword-only. The first five arguments
are the ones released on master and can still be passed by position.
Add a "Which drag is used" section to the generic surfaces guide: the
rocket's own drag curves always apply, every surface's axial force is
added on top, and `overwrite=True` in `add_full_body_aerodynamics`
removes the existing surfaces and zeroes both drag curves. Warn that
full-vehicle data that already includes drag is counted twice otherwise.

Also fix a typo ("collapses" to "collapse").
- Remove unused imports and the unused `plane` argument of
  `_draw_center_of_mass_and_pressure`.
- Give every `to_dict` override the `include_outputs` argument of its
  base class.
- Explain and disable the warnings that misread a deliberate pattern:
  `_BaseFin` initializes `GenericSurface` later, `Fin` and `Fins` are
  still abstract, and `u_dot_parachute` keeps the shared `t` argument.
- Disable too-many-lines in `rocket.py` and `flight.py`, too-many-locals
  in `full_body_coefficients` and too-many-statements in
  `AeroCoefficient.__init__`.
- Allow names like `M1_cm` in `.pylintrc`.
- Tests: move imports to the top, use dict literals, drop unused
  variables and arguments.
MateusStano and others added 4 commits October 5, 2026 20:10
dict.update has no inspectable signature, so passing it directly as an
Event callback fails; restore the lambda and silence the pylint hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	rocketpy/plots/aero_surface_plots.py
#	rocketpy/rocket/aero_surface/fins/fin.py
#	rocketpy/rocket/aero_surface/fins/fins.py
#	rocketpy/simulation/helpers/flight_derivatives.py
#	tests/unit/simulation/test_dynamics.py
#	tests/unit/simulation/test_event.py
#	tests/unit/simulation/test_solution.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MateusStano
MateusStano marked this pull request as ready for review October 5, 2026 23:39
@MateusStano
MateusStano requested a review from a team as a code owner October 5, 2026 23:39
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.

4 participants