Skip to content

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

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

ENH: Refactor Aero Surface, Make GenericSurface the Mother Class#1064
MateusStano wants to merge 12 commits into
enh/eventsfrom
enh/aero-refactor

Conversation

@MateusStano

Copy link
Copy Markdown
Member

TBD

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 on lines +359 to +364
The ``reynolds`` axis is by default based on the reference length (the
rocket diameter). Published rocket data often bases the Reynolds number on
the **body length** instead, which for a slender rocket is much larger. If
your table uses a different length, pass it as ``reynolds_length`` when
creating the surface so the Reynolds number the simulation feeds your table
matches the one it was built against.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This phrase is a bit odd, especially the ending "so the Reynolds number the simulation...".

What if the user table is based on the reference length and reynolds_length is given? Good docstrings here is important to avoid confusion.

Comment on lines +679 to +707
Activation Window
-----------------

By default a surface produces aerodynamic force throughout the flight. The
``active_during`` argument restricts it to part of the flight. This is useful
for a surface that only exists (or only matters) during a phase. It is accepted
by both :class:`rocketpy.GenericSurface` and
:class:`rocketpy.LinearGenericSurface`, and accepts:

- ``"always"`` (default): the surface always contributes force.
- ``"power_on"``: only while the motor is burning (up to burnout).
- ``"power_off"``: only after the motor has burned out.
- a callable ``active_during(t, flight)`` returning ``True`` when the surface is
active at time ``t`` (in seconds) of the given :class:`rocketpy.Flight`, for
any custom window.

.. code-block:: python

# base drag that only applies after burnout
base_drag = GenericSurface(
reference_area=rocket.area,
reference_length=2 * rocket.radius,
coefficients={"cA": 0.4}, # axial (drag) coefficient
active_during="power_off",
)

This is also how a full-vehicle model captures the powered/coasting drag
difference: build one ``"power_on"`` and one ``"power_off"`` surface and add them
together (see :ref:`fullbodyaerodynamics`).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be more fitting to accept (also) an Event here? Under the hood, are the default options power_on and power_off converted to a simulation Event?

Comment on lines +736 to +741
Because it is just another aerodynamic surface, a full-vehicle model can be
**mixed** with modelled add-on surfaces (e.g. a measured body plus modelled
canards). Pass ``overwrite=True`` to make it the rocket's **only** aerodynamics:
every existing aerodynamic surface is removed and both built-in drag curves are
cleared, so the supplied surface(s) provide the complete force set.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a small section or a few more words to clarify exactly when which drag is used could be beneficial. If I understood it correctly:

  • Rocket.power_on_drag and Rocket.power_off_drag: are the default and are used when GenericSurfaces are present (e.g. fins, tail, nosecones etc). The other aerodynamic parameters (e.g. lift) do come from a composition of the GenericSurfaces, i.e. only drag is ignored (except if the active_during is given, which adds to the rocket drag);
  • Rocket.add_full_body_aerodynamics with overwrite=False: as a normal GenericSurface, the drag still is the Rocket.power_on_drag and Rocket.power_off_drag. It could be briefly clarified what is the mathematical/modelling difference of this surface to the non-full-body ones;
  • Rocket.add_full_body_aerodynamics with overwrite=True: the Rocket.power_on_drag and Rocket.power_off_drag are ignored in favor of the surface.

Maybe I am mixing things up, but if I got it right, maybe renaming overwrite to overwrite_rocket_drag could help here.

Extracting a rocket's coefficients
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can also collapses an assembled rocket into a single

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
You can also collapses an assembled rocket into a single
You can also collapse an assembled rocket into a single

Comment on lines +688 to +693
- ``"always"`` (default): the surface always contributes force.
- ``"power_on"``: only while the motor is burning (up to burnout).
- ``"power_off"``: only after the motor has burned out.
- a callable ``active_during(t, flight)`` returning ``True`` when the surface is
active at time ``t`` (in seconds) of the given :class:`rocketpy.Flight`, for
any custom window.

@phmbressan phmbressan Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this is rather intuitive for 99% of rockets and users. However I would like to bring to discussion whether the naming/division power_on and power_off is adequate model flexibility and future proofing. I ask this, since it has a few assumptions:

  • Assumes a single engine rocket;
  • Assumes the effect of the engine on drag is always the same, no matter the throttling or TVC.

Maybe there are more. Overall it feels a bit restrained in terms of design (I understand having this as default/pre-built/easy to use options), but having the option to tweak the parameters (not only toggle) based on the any flight parameter or on an Event might be interesting.

Comment on lines +645 to +674
Choosing an extrapolation method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Extrapolation controls the behavior *outside* the tabulated range. The options
are ``"constant"``, ``"natural"`` and ``"zero"``. This choice matters more than
interpolation, because a bad one fails silently, precisely when the rocket is at
an extreme condition beyond your data.

- ``"constant"`` holds the value at the nearest edge of the data. This is the
**default for tabulated coefficients**, and the right choice for essentially
all of them: a rocket can briefly exceed your tabulated Mach/angle range, and
holding the last value is bounded and physically conservative.
- ``"zero"`` returns 0 outside the range. Occasionally reasonable for force or
moment *slopes* if you want contributions to vanish past the modeled envelope,
but it introduces a discontinuity at the edge.
- ``"natural"`` continues the fitted curve past the data. **Avoid this for
tabulated coefficients**: extrapolating a linear or spline fit can send
:math:`C_A` or a moment slope to large, non-physical values right when the
rocket is at an extreme condition.

.. tip::
Tabulated coefficients default to ``extrapolation="constant"`` so they never
run to non-physical values past the tabulated envelope. Override it only when
you have a specific reason (e.g. ``"zero"`` to make a contribution vanish
outside the modeled range).

.. seealso::
These arguments are forwarded to each :class:`rocketpy.Function`; see
:meth:`rocketpy.Function.set_interpolation` and
:meth:`rocketpy.Function.set_extrapolation` for the full list of methods.

@phmbressan phmbressan Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, should the recommendation of "constant" extrapolation be valid for both LinearGenericSurface (based on derivatives) and GenericSurface? I ask this, since maybe it is a matter of keeping consistency: if a coefficient has a constant extrapolation, its derivative extrapolation should likely be zero.

I don't know what is more appropriate in this case, but in the Motor class, for instance, the Motor.propellant_mass has constant extrapolation, whilst the Motor.net_mass_flow_rate has zero extrapolation exactly for this reason. I don't know, maybe the most logical for this surfaces case would be constant extrapolation for derivative coefficients and (consequentially) linear extrapolation for the normal coefficients.

Comment on lines +246 to +247
extrapolation=extrapolation,
interpolation=interpolation,

@phmbressan phmbressan Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably make these two **kwargs or after a ,*,. I believe their use here is more justified, but the same parameter is present as positional in the Motor class and I feel it is very underused/not useful there.

center_of_pressure=(self.cpx, self.cpy, self.cpz),
name=self.name,
)
self._barrowman_initialized = True

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The architecture here is a bit convoluted, I understand the reasoning, but it is hard to follow due to the amount of methods that need to be called at specific places in the code (e.g. before barrowman/post barrowman) and that have similar names (update_coefficient/calculate_all_coefficients etc).

Another factor _update_geometry_chain() sometimes constructs the superclass and sometimes updates it, which also involves a fair bit of care.

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.

2 participants