Skip to content

ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis) - #920

Merged
Gui-FernandesBR merged 7 commits into
RocketPy-Team:developfrom
Rafit345:enh/tip-off-analysis-3-dof
Sep 14, 2026
Merged

Gui-FernandesBR merged 7 commits into
RocketPy-Team:developfrom
Rafit345:enh/tip-off-analysis-3-dof

Conversation

@Rafit345

@Rafit345 Rafit345 commented Dec 10, 2025 •

Copy link
Copy Markdown

Pull request type

  • Code changes (bugfix, features)
  • Code maintenance (refactoring, formatting, tests)
  • ReadMe, Docs and GitHub updates

Checklist

  • Tests for the changes have been added (if needed)
  • Docs have been reviewed and added / updated
  • Lint (ruff check, ruff format --check, pylint) has passed locally
  • All tests have passed locally
  • CHANGELOG.md has been updated (if relevant)

Current behavior

Refs #28.

The rail phase ends the moment the upper rail button reaches the end of the rail,
at effective_1rl, and the simulation jumps straight from 1-DOF rail motion to
6-DOF free flight. The interval in which the rocket is still guided by the lower
button alone — the tip-off phase — is not modelled, so the rocket begins free
flight with no angular rate and with exactly the rail's attitude.

New behavior

Adds Flight.udot_rail2, an intermediate 3-DOF flight phase covering the interval
between the two rail buttons leaving the rail:

udot_rail1        for d <  effective_1rl   (both buttons engaged, 1 DOF)
udot_rail2        for d in [effective_1rl, effective_2rl)  (lower button only, 3 DOF)
u_dot_generalized for d >= effective_2rl   (free flight, 6 DOF)

The phase models a constrained rigid body: the lower button slides along the fixed
inertial rail axis and roll is suppressed, leaving translation along the rail, pitch
and yaw. It reuses u_dot_generalized for the free solution and adds a reaction
wrench — a normal force at the button (perpendicular to the rail, 2 DOF) plus a roll
reaction moment (1 DOF) — solved from a 3x3 linear system built from the three
constraints: the button's perpendicular acceleration vanishes (2) and the roll angular
acceleration vanishes (1). Aerodynamics, thrust, body-frame gravity and the evolving
inertia tensor therefore come along by construction.

The derivation is documented in docs/technical/tip_off.rst.

Enabled with Flight(..., use_udot_rail2=True).

Breaking change

  • Yes
  • No

use_udot_rail2 defaults to False, and a disabled run is bit-for-bit identical
to develop — verified by comparing the full final state against a clean develop
checkout, not just the apogee.

Additional information

Validation

  • Constraint satisfaction: the button's perpendicular offset from the rail stays at
    ~1e-13 m; roll rate and roll acceleration stay at zero.
  • Physics: with no wind the nose pitches down (the center of mass is ahead of the
    pivot); with a crosswind the rocket weathercocks into the wind before fully leaving
    the rail, which is the acceptance criterion in ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis) #28. Apogee shifts about -2 m on
    Calisto.
  • 1960 unit and simulation-integration tests pass locally; all CI checks pass.

Also fixed here: a Flight continued from another Flight object never set
t_initial, which raised AttributeError for any continued flight carrying sensors
or controllers. The bug predates this branch and is fixed by merging the two
initial-solution branches (a refactor the review asked for). Covered by a regression
test.

Known limitation: the button is modelled on the rocket axis, so the small roll
coupling through its radial standoff is not represented — roll is instead suppressed
explicitly by the reaction moment. Documented as a modelling assumption; a natural
follow-up.

References: "Tip-off effect analysis of a vehicle moving along an inclined
guideway by considering dynamic interactions" (Chou et al.) and "Analysis of Missile
Launchers Part Q: Tipoff Effects in Helical Rail Launchers" (Hosken et al.).

@Rafit345
Rafit345 requested a review from a team as a code owner December 10, 2025 23:47
@Rafit345
Rafit345 changed the base branch from master to develop December 10, 2025 23:47
@aZira371

Copy link
Copy Markdown
Collaborator

Hey @Rafit345 Thanks for this PR! You have done some solid work here. I am still reviewing your code changes. But I'll suggest to run make lint and make format with your commits!

@aZira371
aZira371 self-requested a review December 12, 2025 16:37
Comment thread rocketpy/simulation/flight.py Outdated
print(f"Current Simulation Time: {self.t:3.4f} s", end="\r")

# check for the first time the rocket is between the two rail buttons for tip-off analysis
if len(self.between_rails_state) == 1 and (

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 condition len(self.betweenrailsstate) == 1 checks whether the state has been set once, not whether the rocket is currently between the buttons. Once betweenrailsstate is populated, subsequent integration steps will not re-enter this block, so the phase is only added at the exact instant the lower button clears, not maintained throughout the tip-off interval. This means the udot_rail2 phase may execute for only a single time step or not propagate correctly.

@aZira371 aZira371 left a comment

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 have the following comments for the preliminary implementation. In general looks to be going in a good direction as of now. Will wait for u_dotrail2 implementation to be fully complete in order to review the tests and involved physics.

Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread tests/unit/simulation/test_udot_rail2_feature.py Outdated
@Gui-FernandesBR

Copy link
Copy Markdown
Member

@Rafit345 thank you for your submission! Please address all the comments and ask for a re-review whenever this PR is ready again.

We look forwarding to receiving updates from you soon!

@Gui-FernandesBR
Gui-FernandesBR marked this pull request as draft December 14, 2025 17:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a preliminary 3-DOF single rail button flight phase (tip-off analysis) for RocketPy, introducing an intermediate udot_rail2 phase that operates between the initial 1-DOF rail phase and the 6-DOF free flight phase. The implementation adds a feature flag use_udot_rail2 to enable/disable this behavior, includes a Hermite-root fallback for numerical robustness, and provides comprehensive unit tests to verify correctness.

  • Adds udot_rail2 method implementing 3-DOF equations of motion (linear motion along rail + pitch/yaw, enforcing zero roll)
  • Introduces use_udot_rail2 parameter (default True) to control the intermediate rail phase activation
  • Implements between-rails event detection and smooth phase transitions from 1-DOF → 3-DOF → 6-DOF
  • Adds fallback handling for edge cases where rail-exit root filtering returns no valid roots

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 15 comments.

File Description
rocketpy/simulation/flight.py Implements udot_rail2 3-DOF rail phase with equations of motion, adds use_udot_rail2 parameter, implements between-rails event detection, adds fallback for root finding failures, initializes attitude_unit and between_rails_state tracking
tests/unit/simulation/test_udot_rail2_feature.py Adds unit tests verifying phase insertion order, zero roll enforcement, rail alignment constraints, and CSV output generation for enabled/disabled comparison

Comment thread tests/unit/simulation/test_udot_rail2_feature.py Outdated
Comment thread tests/unit/simulation/test_udot_rail2_feature.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py Outdated
Comment thread tests/unit/simulation/test_udot_rail2_feature.py Outdated
Comment thread tests/unit/simulation/test_udot_rail2_feature.py Outdated
@Gui-FernandesBR Gui-FernandesBR linked an issue Dec 21, 2025 that may be closed by this pull request
4 tasks
@Gui-FernandesBR Gui-FernandesBR changed the title "ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis) - Preliminary" ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis) Jul 4, 2026
@Gui-FernandesBR
Gui-FernandesBR force-pushed the enh/tip-off-analysis-3-dof branch from 90813ac to a2c5b42 Compare July 22, 2026 15:18
@codecov

codecov Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.47899% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.49%. Comparing base (88aed5a) to head (f66bc58).

Files with missing lines Patch % Lines
rocketpy/simulation/flight.py 98.03% 2 Missing ⚠️
rocketpy/plots/flight_plots.py 87.50% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #920      +/-   ##
===========================================
+ Coverage    91.41%   91.49%   +0.07%     
===========================================
  Files          132      132              
  Lines        18133    18241     +108     
===========================================
+ Hits         16577    16689     +112     
+ Misses        1556     1552       -4     

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

Rafit345 and others added 4 commits August 8, 2026 16:19
…is)"

Refs RocketPy-Team#28.  This commit was made as a submission
to the selective process deliverables challenge.

The method udot_rail2 functions as an intermediate flight phase
before the rocket has fully left the guide rail, allowing for
3 degrees of freedom (linear motion along the rail, pitch and yaw).

Flight init includes a feature to run a simulation without udot_rail2.
Numerical values enabling udot_rail2 are very close to 1 DOF flight.
Flight phase transitions smoothly from 1 DOF rail phase to 3DOF
and from 3 DOF to 6 DOF free flight.

Current equations of motion inside udot_rail2 rely heavily on
udot_generalized, ensuring 3 DOF through vector operations.
Still working on the implementation of proper lagrangean expansion
/derivation of equations of motion. Articles
"Tip-off effect analysis of a vehicle moving along an inclined guideway
by considering dynamic interactions" by Chou et al and
"ANALYSIS OF MISSILE LAUNCHERS PART Q
Tipoff Effects in Helical Rail Launchers" by  Hosken et al
are proving useful.

--Summary--
Add preliminary udot_rail2 (3-DOF tip-off) support and safe,
deterministic phase-insertion handling during rail → 6DOF transitions.
Add a feature flag to enable/disable udot_rail2 on Flight init.
Add a Hermite-root fallback to avoid hard failures when rail-exit root
 filtering returns no valid root (warn + midpoint fallback).
Add comprehensive unit tests (alignment, no-roll, insertion-order,
CSV comparisons) and sample CSV output for comparison runs with udot_rail2
 enabled vs disabled.
Complete the 3-DOF single-rail-button (tip-off) phase from issue RocketPy-Team#28.

- Fix the phase transition ordering: rail1 -> udot_rail2 (at effective_1rl,
  upper button exit) -> u_dot_generalized (at effective_2rl, lower button
  exit). Previously the thresholds were swapped, so udot_rail2 was inserted
  after free flight and never exited.
- Replace the placeholder udot_rail2 (which reused free-flight dynamics with an
  ad-hoc velocity projection) with rigorous constrained dynamics: the lower
  button slides along the fixed rail while roll is suppressed. The reaction
  wrench (normal force + roll moment) is solved from a 3x3 linear system so the
  button's perpendicular acceleration and the roll acceleration vanish, derived
  in the true body frame on top of the validated u_dot_generalized solution.
- Make the feature opt-in (use_udot_rail2 defaults to False); disabled runs are
  bit-for-bit identical to previous behavior.
- Factor the rail-exit root finding into a shared helper.
- Rewrite the unit tests to check phase ordering, the opt-in default, the
  on-rail constraint (button stays on the rail to machine precision), zero
  roll, and the gravity tip-off direction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Define the rail axis (`attitude_unit`) for every Flight, not only the ones
  that start on the rail. It depends solely on the launch inclination and
  heading, so `udot_rail2` no longer raises `AttributeError` when an
  `initial_solution` skips the rail phase. Verified equal to the previous
  quaternion-derived vector to 3e-16 across inclinations, headings and rolls.
- Compute the squared distance from the launch point once and share it between
  the two rail button exit checks.
- Rename `r_B` -> `r_button` and `I_CM_inv` -> `inv_inertia_cm`, drop the
  unused unpacking in `udot_rail2` and the now-dead `K_init`, so pylint is
  clean without relaxing `.pylintrc`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The udot_rail2 docstring pointed at a derivation that lived in an untracked
scratch file, so the reference was dead for anyone reading the code. Move the
derivation into the technical documentation, where the other equations of
motion are documented, and cite the two tip-off papers it follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Gui-FernandesBR
Gui-FernandesBR force-pushed the enh/tip-off-analysis-3-dof branch from a2c5b42 to 80ff5da Compare August 10, 2026 02:37
Fold the two initial-solution branches of __init_flight_state into one, as the
review asked: they set the same monitors, and the Flight-object branch differed
only by *not* assigning t_initial. That omission raised
`AttributeError: 'Flight' object has no attribute 't_initial'` whenever the
continued rocket carried sensors or controllers, since post-processing the
initial state reads it. The bug predates this branch; merging the branches fixes
it. Covered by a regression test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Gui-FernandesBR

Copy link
Copy Markdown
Member

@aZira371 @Rafit345 this PR is ready for another look. Since the last review the
udot_rail2 physics was rewritten, the phase transition was fixed, and the branch
was rebased onto current develop (no conflicts). All checks are green.

The two blocking problems

1. The phase transition was inverted. The thresholds were swapped: between_rails
keyed on effective_2rl as the leading if, out_of_rail on the smaller
effective_1rl as the elif. So out_of_rail fired first and udot_rail2 was
inserted after free flight, where it never exited. The observed phase order was
rail1 -> u_dot_generalized -> udot_rail2. It is now
rail1 -> udot_rail2 (at effective_1rl, upper button out) -> u_dot_generalized
(at effective_2rl, lower button out), which is also what broke ~32 existing flight
tests with ValueError: No valid roots found when solving for rail exit time.

2. The dynamics had no reaction force, which is the substance of your review
comment: it reused free-flight dynamics plus an ad-hoc roll-zeroing and a projection
of the velocity onto the rail. That is now replaced by proper constrained dynamics,
along the lines you asked for:

  • The lower button slides along the fixed inertial rail axis; roll is suppressed.
  • The unknowns are a normal reaction force at the button (perpendicular to the
    rail, 2 DOF) and a roll reaction moment (1 DOF).
  • They are solved from a 3x3 linear system built from the three constraints: the
    button's acceleration perpendicular to the rail vanishes (2) and the roll angular
    acceleration vanishes (1). 6 - 3 = 3 DOF remain: slide along the rail, pitch, yaw.
  • It builds on u_dot_generalized, so the off-axis aerodynamics, thrust,
    body-frame gravity and the evolving inertia tensor you listed are all included by
    construction rather than re-derived. The reaction wrench is added to the same
    T20/T21 totals and the same solve is applied.
  • The velocity is not projected onto the rail any more. The constraint acts at
    the acceleration level on the button; the CDM legitimately acquires a small
    perpendicular velocity as the rocket pivots.

The derivation is now in the technical documentation, docs/technical/tip_off.rst,
next to the other equations of motion.

Your other comments

  • Midpoint fallback with no physical justification - removed. The root finder was
    factored into one shared helper (__root_find_rail_exit_time) used by both events,
    and it raises rather than guessing.
  • len(between_rails_state) == 1 does not mean "currently between the buttons" -
    correct, and that is the intent: it is a "has this event fired yet" guard, the same
    idiom as the pre-existing len(out_of_rail_state) == 1. What keeps the phase active
    is the phase itself, not this check. It now also requires out_of_rail to have
    already fired, so it can only trigger inside the tip-off window.
  • Redundant between_rails initialization across three branches - fixed. The two
    initial-solution branches are now one. Worth flagging: they were not identical - the
    Flight-object branch also failed to set t_initial, which raised
    AttributeError: 'Flight' object has no attribute 't_initial' for any continued
    flight carrying sensors or controllers. That bug predates this branch; merging the
    branches fixes it, and there is a regression test.
  • CSV test asserts nothing - agreed, it was structural. The test file was rewritten
    around behaviour: phase ordering, the opt-in default, the on-rail constraint, zero
    roll, the gravity tip-off direction, and the rail axis being defined when an
    initial_solution skips the rail. No CSV artifacts.
  • Copilot's points - use_udot_rail2 now defaults to False, the docstring entry
    moved to match the signature order, the leftover commented-out code and unused
    imports/variables are gone, the pragma: no cover on udot_rail2 was dropped, and
    the test names follow test_methodname_expectedbehaviour.

Two Copilot comments I deliberately left alone: the a += 1e-5 # TODO: why?? in the
root finder and the negative vz doesn't really mean apogee TODO. Both are
pre-existing on develop and unrelated to tip-off; fixing them here would change
results for every flight and belongs in its own PR.

Validation

  • use_udot_rail2=False is bit-for-bit identical to develop - verified by
    running the same scenario against a clean develop worktree and comparing the full
    final state, not just the apogee. This is why the feature is opt-in for now.
  • Constraint satisfaction: the button's perpendicular offset from the rail stays at
    ~1e-13 m, and the roll rate and roll acceleration stay at zero.
  • Physics: with no wind the nose pitches down (the center of mass is ahead of the
    pivot), and the rocket weathercocks into a crosswind - the acceptance criterion in
    ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis) #28. Apogee shifts by about -2 m on Calisto.
  • Suites: 1960 unit + simulation-integration tests pass locally; all 10 CI checks pass.

Known limitation

The button is modelled on the rocket axis, so the small roll coupling through its
radial standoff is not represented; roll is instead suppressed explicitly by the
reaction moment. This is documented as a modelling assumption and is a natural
follow-up.

@Gui-FernandesBR
Gui-FernandesBR marked this pull request as ready for review August 10, 2026 14:58
Comment thread rocketpy/simulation/flight.py Outdated
@Gui-FernandesBR

Copy link
Copy Markdown
Member

Status check on this one: it went conflicting with develop. @Rafit345 could you merge develop in and push? All 19 checks were green before the conflict appeared, so this should just be a base update.

@aZira371 the CHANGES_REQUESTED review here dates from 2025-12-13 and a lot of commits have landed since then. Once the conflicts are resolved, could you take another look? That stale review is currently the only thing blocking this PR.

@aZira371

Copy link
Copy Markdown
Collaborator

Status check on this one: it went conflicting with develop. @Rafit345 could you merge develop in and push? All 19 checks were green before the conflict appeared, so this should just be a base update.

@aZira371 the CHANGES_REQUESTED review here dates from 2025-12-13 and a lot of commits have landed since then. Once the conflicts are resolved, could you take another look? That stale review is currently the only thing blocking this PR.

I'll review this soon!

Comment thread rocketpy/simulation/flight.py Outdated
Comment thread rocketpy/simulation/flight.py
Comment thread rocketpy/simulation/flight.py
Comment thread rocketpy/simulation/flight.py Outdated
Gui-FernandesBR and others added 2 commits September 13, 2026 23:23
# Conflicts:
#	CHANGELOG.md
#	rocketpy/simulation/flight.py
#	tests/unit/simulation/test_flight.py
Picks up the four unresolved review threads on RocketPy-Team#920, on top of develop.

Blocking:

* Move ``use_udot_rail2`` to the end of ``Flight.__init__``. It sat between
  ``equations_of_motion`` and ``ode_solver``, so any caller passing
  ``ode_solver``, ``simulation_mode`` or ``post_step_callback`` positionally
  silently got the wrong value. The docstring entry moves with it.

Correctness:

* Refuse ``use_udot_rail2=True`` together with ``simulation_mode="3 DOF"`` or
  ``equations_of_motion="solid_propulsion"``. The phase patches the generalized
  6-DOF solution with a constraint wrench built from the full inertia tensor,
  but those options rebind ``u_dot_generalized`` to reduced formulations that do
  not carry that state -- ``u_dot_generalized_3dof`` models no attitude at all.
  The combination was neither guarded nor tested; it now raises ValueError.
  Note this also covers point-mass motors, which force "3 DOF".

* Give ``use_udot_rail2`` a class-level default. Flights restored from a ``.rpy``
  written before this feature are rebuilt without ``__init__``, so reading the
  flag raised AttributeError (caught by test_load_from_rpy).

Reporting:

* ``between_rails_time`` and ``between_rails_state`` were tracked and
  serialized, but never surfaced: studying tip-off for dispersion meant digging
  them out of the raw solution. Adds ``between_rails_velocity`` and
  ``tip_off_duration``, a "Tip-Off State" section in ``Flight.info()``, and
  shading of the window in the attitude plots. All of it is inert when the
  phase is off, so existing output is unchanged.

Performance:

* ``udot_rail2`` re-interpolated the total mass and the inertia tensor that
  ``u_dot_generalized`` had just computed for the same t, on every solver
  evaluation inside the window. The generalized equations now expose both.
  Verified to leave the trajectory bit-for-bit identical.

Adds eight tests covering the guard, the reporting, and the restored-object
path. Documents the mode restrictions and the reported attributes in the
technical docs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Gui-FernandesBR
Gui-FernandesBR merged commit ebe1b77 into RocketPy-Team:develop Sep 14, 2026
9 checks passed
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.

ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis)

4 participants