Skip to content

Commit ebe1b77

Browse files
Rafit345Gui-FernandesBRclaude
authored
ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis) (#920)
* "ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis)" Refs #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. * ENH: derive constrained equations of motion for udot_rail2 tip-off phase Complete the 3-DOF single-rail-button (tip-off) phase from issue #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> * MNT: address the udot_rail2 review comments - 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> * DOC: document the tip-off equations of motion 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> * BUG: record t_initial when a flight continues from another Flight 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> * MNT: address the outstanding review points on the tip-off phase Picks up the four unresolved review threads on #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> --------- Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9fbf3a3 commit ebe1b77

9 files changed

Lines changed: 1173 additions & 32 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Attention: The newest changes should be on top -->
3232

3333
### Added
3434

35+
- ENH: 3-DOF single rail button flight phase (tip-off analysis), enabled with the opt-in `Flight(use_udot_rail2=True)`. Between the upper rail button leaving the rail and the lower one following it, the rocket pivots about the lower button under a solved constraint wrench instead of jumping straight to free 6-DOF flight. The window is reported as `between_rails_time`, `between_rails_velocity` and `tip_off_duration`, printed by `Flight.info()` and shaded in the attitude plots. Requires `simulation_mode="6 DOF"` and `equations_of_motion="standard"`. [#920](https://github.com/RocketPy-Team/RocketPy/pull/920)
3536
- ENH: Support fixed-time parachute deployment triggers [#1133](https://github.com/RocketPy-Team/RocketPy/pull/1133) [#437](https://github.com/RocketPy-Team/RocketPy/issues/437)
3637
- DOC: Add SIL parachute ejection integration example [#1131](https://github.com/RocketPy-Team/RocketPy/pull/1131) [#524](https://github.com/RocketPy-Team/RocketPy/issues/524)
3738
- ENH: List NOAA atmosphere datasets and fetch latest [#1136](https://github.com/RocketPy-Team/RocketPy/pull/1136) [#660](https://github.com/RocketPy-Team/RocketPy/issues/660)
@@ -61,6 +62,7 @@ Attention: The newest changes should be on top -->
6162

6263
### Fixed
6364

65+
- BUG: A `Flight` continued from another `Flight` object now records `t_initial`, so a rocket carrying sensors or controllers no longer raises `AttributeError` on that path. [#920](https://github.com/RocketPy-Team/RocketPy/pull/920)
6466
- BUG: Correct the gravity sign an `Accelerometer` applies when `consider_gravity=True`. The gravitational field was added to the inertial acceleration instead of subtracted from it, so the sensor reported the negative of the proper acceleration along the vertical: one at rest read -g rather than +g. Recorded accelerometer data taken with `consider_gravity=True` changes sign in that term. [#1175](https://github.com/RocketPy-Team/RocketPy/pull/1175)
6567
- BUG: Report a Monte Carlo worker that fails instead of hanging or passing for a finished run [#1182](https://github.com/RocketPy-Team/RocketPy/pull/1182)
6668
- BUG: Sample `StochasticFlight` inputs once per simulation [#1126](https://github.com/RocketPy-Team/RocketPy/pull/1126) [#1090](https://github.com/RocketPy-Team/RocketPy/issues/1090)

‎docs/technical/index.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ in their code.
1212

1313
Equations of Motion v0 <equations_of_motion.rst>
1414
Equations of Motion v1 <equations_of_motion_v1.rst>
15+
Tip-off <tip_off.rst>
1516
Elliptical Fins <aerodynamics/elliptical_fins.rst>
1617
Individual Fin <aerodynamics/individual_fins.rst>
1718
Tube Fins <aerodynamics/tube_fins.rst>

‎docs/technical/references.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ References
66
.. [Niskanen] Niskanen, S. (2013). *Development of an Open Source model rocket simulation software*.
77
88
.. [Model] Barrowman, James S.. (1970). *Model Rocketry*.
9+
10+
.. [Chou] Chou, P.-C., et al. *Tip-off effect analysis of a vehicle moving along an inclined guideway by considering dynamic interactions*.
11+
12+
.. [Hosken] Hosken, R. W., et al. *Analysis of missile launchers, part Q: tip-off effects in helical rail launchers*.

‎docs/technical/tip_off.rst‎

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
.. _tipoff:
2+
3+
===========================================
4+
Tip-off: the 3-DOF Single Rail Button Phase
5+
===========================================
6+
7+
Introduction
8+
------------
9+
10+
Between the moment the *upper* rail button leaves the launch rail and the moment
11+
the *lower* button follows it, the rocket is still guided --- but only at one
12+
point. It slides along the rail while free to pitch and yaw about that remaining
13+
button. This short interval is what the literature calls **tip-off**, and it sets
14+
the attitude and angular rate with which the rocket begins free flight.
15+
16+
This document derives the equations of motion used by
17+
:meth:`rocketpy.Flight.udot_rail2`, the flight phase that models this interval.
18+
It is enabled with ``Flight(..., use_udot_rail2=True)``; when disabled (the
19+
default) the simulation transitions straight from the 1-DOF rail phase to the
20+
generalized 6-DOF equations, exactly as it did before this phase existed.
21+
22+
Because the phase works by patching the *generalized* 6-DOF solution with a
23+
constraint wrench built from the full inertia tensor, it requires
24+
``simulation_mode="6 DOF"`` and ``equations_of_motion="standard"``. The reduced
25+
formulations do not carry the state the patch describes --- the 3 DOF equations
26+
model no attitude at all, and ``solid_propulsion`` uses a different,
27+
axisymmetric set --- so asking for the tip-off phase together with either of
28+
them raises a ``ValueError`` instead of silently producing non-physical
29+
kinematics. Note that a point-mass motor forces ``simulation_mode`` to
30+
``"3 DOF"``, and therefore cannot be combined with this phase either.
31+
32+
The three flight phases around rail departure are, in order of the distance
33+
``d`` travelled from the launch point:
34+
35+
.. math::
36+
37+
\begin{aligned}
38+
\texttt{udot_rail1} \quad & \text{for } d < \ell_1
39+
&& \text{(both buttons engaged, 1 DOF)} \\
40+
\texttt{udot_rail2} \quad & \text{for } \ell_1 \le d < \ell_2
41+
&& \text{(upper button gone, lower engaged, 3 DOF)} \\
42+
\texttt{u_dot_generalized} \quad & \text{for } d \ge \ell_2
43+
&& \text{(free flight, 6 DOF)}
44+
\end{aligned}
45+
46+
where :math:`\ell_1` and :math:`\ell_2` are the ``effective_1rl`` and
47+
``effective_2rl`` attributes of :class:`rocketpy.Flight` --- the distances at
48+
which the upper and the lower button reach the end of the rail. Their difference
49+
is the button-to-button distance, so the phase has zero length for a rocket with
50+
a single rail button and is skipped in that case.
51+
52+
Frames and conventions
53+
----------------------
54+
55+
The solver integrates the same 13-element state vector used by the other
56+
right-hand sides,
57+
58+
.. math::
59+
60+
\mathbf{u} = [\,x,\ y,\ z,\ v_x,\ v_y,\ v_z,\ e_0,\ e_1,\ e_2,\ e_3,\
61+
\omega_1,\ \omega_2,\ \omega_3\,]
62+
63+
with :math:`\mathbf{r} = [x, y, z]` the inertial position of the **center of dry
64+
mass** (CDM, the tracked point), :math:`\mathbf{v}` its inertial velocity,
65+
:math:`\mathbf{e}` the attitude quaternion and :math:`\boldsymbol{\omega}` the
66+
angular velocity in the **body** frame. The matrix
67+
:math:`\mathbf{K} = \texttt{Matrix.transformation}(\mathbf{e})` rotates body
68+
components into inertial ones, and :math:`\hat{\mathbf{z}}_b = [0, 0, 1]` is the
69+
body roll (symmetry) axis.
70+
71+
The relevant mass geometry at time :math:`t`, in the body frame, is the total
72+
mass :math:`m`, the CDM-to-center-of-mass offset :math:`\mathbf{r}_{CM}`, and the
73+
inertia tensor about the CDM, :math:`\mathbf{I}`. Shifting the latter to the
74+
instantaneous center of mass gives
75+
76+
.. math::
77+
78+
\mathbf{I}_{CM} = \mathbf{I} - m\left(|\mathbf{r}_{CM}|^2 \mathbb{1}
79+
- \mathbf{r}_{CM}\mathbf{r}_{CM}^{\mathsf T}\right).
80+
81+
**Rail geometry.** The rail is a line fixed in the inertial frame, set by the
82+
launch inclination and heading. Its unit vector is the ``attitude_unit``
83+
attribute of :class:`rocketpy.Flight`; the constrained body point is the lower
84+
rail button, at the fixed body position :math:`\mathbf{r}_{B}` relative to the
85+
CDM.
86+
87+
The constraint
88+
--------------
89+
90+
A free rigid body has six degrees of freedom. The single engaged button removes
91+
three of them:
92+
93+
#. **The button stays on the rail line.** Its position may only vary along the
94+
rail, so the component of its acceleration perpendicular to the rail
95+
vanishes. That is **two** scalar constraints.
96+
#. **Roll is suppressed** by the button in its rail slot:
97+
:math:`\dot{\boldsymbol{\omega}} \cdot \hat{\mathbf{z}}_b = 0`. That is
98+
**one** more.
99+
100+
Three constraints leave :math:`6 - 3 = 3` degrees of freedom: translation along
101+
the rail, pitch and yaw --- the 3 DOF this phase is named for.
102+
103+
The forces that enforce them are the unknowns of the problem:
104+
105+
* a **normal reaction** at the button, perpendicular to the rail because a
106+
frictionless slot can neither pull nor push along it. Writing an orthonormal
107+
body triad :math:`\{\hat{\mathbf{e}}_1, \hat{\mathbf{e}}_2, \hat{\mathbf{n}}_b\}`
108+
around the body-frame rail direction
109+
:math:`\hat{\mathbf{n}}_b = \mathbf{K}^{\mathsf T}\hat{\mathbf{n}}`, it has two
110+
components: :math:`\mathbf{N}_b = \lambda_1 \hat{\mathbf{e}}_1 + \lambda_2
111+
\hat{\mathbf{e}}_2`;
112+
* a **roll reaction moment** :math:`\mu \hat{\mathbf{z}}_b` about the body axis.
113+
114+
Three unknowns, three constraints: a :math:`3\times3` linear system, solved once
115+
per evaluation of the right-hand side.
116+
117+
The initial conditions are consistent with the constraint. The phase is entered
118+
from ``udot_rail1``, where the rocket has no angular velocity and its velocity is
119+
along the rail, so the button's perpendicular *velocity* is already zero.
120+
Enforcing zero perpendicular *acceleration* therefore keeps it on the rail.
121+
122+
Augmented dynamics
123+
------------------
124+
125+
The generalized equations of motion assemble a total force
126+
:math:`\mathbf{T}_{20}` and a total moment about the CDM :math:`\mathbf{T}_{21}`,
127+
both in the body frame, and solve
128+
129+
.. math::
130+
131+
\dot{\boldsymbol{\omega}}_{\text{free}} = \mathbf{I}_{CM}^{-1}
132+
\left(\mathbf{T}_{21} + \mathbf{T}_{20} \times \mathbf{r}_{CM}\right),
133+
\qquad
134+
\mathbf{a}_{\text{free}} = \frac{\mathbf{T}_{20}}{m}
135+
- \mathbf{r}_{CM} \times \dot{\boldsymbol{\omega}}_{\text{free}}.
136+
137+
Both totals are *sums of external contributions*, which is what makes the
138+
constraint clean to add: the reaction wrench simply enters the sums,
139+
140+
.. math::
141+
142+
\mathbf{T}_{20}' = \mathbf{T}_{20} + \mathbf{N}_b,
143+
\qquad
144+
\mathbf{T}_{21}' = \mathbf{T}_{21} + \mathbf{r}_{B} \times \mathbf{N}_b
145+
+ \mu \hat{\mathbf{z}}_b,
146+
147+
after which the same two lines apply. Since the solve is linear in the totals,
148+
the result splits into the free solution plus a response to the unknowns. Using
149+
:math:`\mathbf{r}_B \times \mathbf{N} + \mathbf{N} \times \mathbf{r}_{CM}
150+
= (\mathbf{r}_B - \mathbf{r}_{CM}) \times \mathbf{N}` and writing
151+
:math:`\mathbf{d} = \mathbf{r}_{B} - \mathbf{r}_{CM}` for the center-of-mass-to-button
152+
vector,
153+
154+
.. math::
155+
156+
\Delta\dot{\boldsymbol{\omega}} = \mathbf{I}_{CM}^{-1}
157+
\left(\mathbf{d} \times \mathbf{N}_b + \mu \hat{\mathbf{z}}_b\right),
158+
\qquad
159+
\Delta\mathbf{a} = \frac{\mathbf{N}_b}{m}
160+
- \mathbf{r}_{CM} \times \Delta\dot{\boldsymbol{\omega}}.
161+
162+
The button is body-fixed, so its acceleration in body components is
163+
164+
.. math::
165+
166+
\mathbf{A} = \mathbf{A}_{\text{free}} + \Delta\mathbf{a}
167+
+ \Delta\dot{\boldsymbol{\omega}} \times \mathbf{r}_{B},
168+
\qquad
169+
\mathbf{A}_{\text{free}} = \mathbf{K}^{\mathsf T}\mathbf{a}_{\text{free}}
170+
+ \dot{\boldsymbol{\omega}}_{\text{free}} \times \mathbf{r}_{B}
171+
+ \boldsymbol{\omega} \times
172+
\left(\boldsymbol{\omega} \times \mathbf{r}_{B}\right).
173+
174+
The linear solve
175+
----------------
176+
177+
Collect the unknowns in :math:`\boldsymbol{\chi} = [\lambda_1, \lambda_2, \mu]`.
178+
The three constraints read
179+
180+
.. math::
181+
182+
\mathbf{A} \cdot \hat{\mathbf{e}}_1 = 0,
183+
\qquad
184+
\mathbf{A} \cdot \hat{\mathbf{e}}_2 = 0,
185+
\qquad
186+
\dot{\boldsymbol{\omega}} \cdot \hat{\mathbf{z}}_b = 0,
187+
188+
and because :math:`\mathbf{A}` and :math:`\dot{\boldsymbol{\omega}}` are linear
189+
in :math:`\boldsymbol{\chi}`, they form the system
190+
:math:`\mathbf{J}\boldsymbol{\chi} = -\mathbf{g}_{\text{free}}` with
191+
192+
.. math::
193+
194+
\mathbf{g}_{\text{free}} =
195+
\begin{bmatrix}
196+
\mathbf{A}_{\text{free}} \cdot \hat{\mathbf{e}}_1 \\
197+
\mathbf{A}_{\text{free}} \cdot \hat{\mathbf{e}}_2 \\
198+
\dot{\boldsymbol{\omega}}_{\text{free}} \cdot \hat{\mathbf{z}}_b
199+
\end{bmatrix}.
200+
201+
Each column of :math:`\mathbf{J}` is obtained by evaluating the response above at
202+
one of the three unit inputs :math:`(\mathbf{N}_b, \mu) =
203+
(\hat{\mathbf{e}}_1, 0)`, :math:`(\hat{\mathbf{e}}_2, 0)`,
204+
:math:`(\mathbf{0}, 1)`. Solving for :math:`\boldsymbol{\chi}` and substituting
205+
back gives the constrained accelerations, which override the free ones in the
206+
returned derivative:
207+
208+
.. math::
209+
210+
\dot{\boldsymbol{\omega}} = \dot{\boldsymbol{\omega}}_{\text{free}}
211+
+ \Delta\dot{\boldsymbol{\omega}},
212+
\qquad
213+
\mathbf{a}_{CDM} = \mathbf{a}_{\text{free}}
214+
+ \mathbf{K}\,\Delta\mathbf{a}.
215+
216+
The position and quaternion derivatives are the ordinary kinematic ones. In
217+
particular :math:`\dot{\mathbf{r}} = \mathbf{v}`: the velocity is **not**
218+
projected onto the rail. The constraint acts at the acceleration level on the
219+
*button*, and the CDM legitimately acquires a small perpendicular velocity as the
220+
rocket pitches about that button.
221+
222+
Modelling assumptions and edge cases
223+
------------------------------------
224+
225+
* **Roll axis.** The constraint suppresses roll about the *body* axis. The rocket
226+
travels only the button-to-button distance during this phase, so the tip-off
227+
angle is small and the body axis stays close to the rail direction; the
228+
difference between body roll and rail-axis roll is of that order. This is
229+
consistent with ``udot_rail1``, which freezes rotation entirely.
230+
* **Radial button offset.** The button is modelled on the rocket axis. The roll
231+
constraint is enforced explicitly by :math:`\mu`, so what is lost is only the
232+
(small) roll coupling through the button's radial standoff.
233+
* **Zero-length phase.** A rocket with a single rail button has
234+
:math:`\ell_1 = \ell_2`, and the phase is skipped.
235+
* **Singular system.** Should the geometry make :math:`\mathbf{J}` singular, the
236+
step falls back to the unconstrained dynamics and warns.
237+
238+
Expected behaviour
239+
------------------
240+
241+
The phase reproduces the two effects tip-off is modelled for. With no wind, the
242+
center of mass sits ahead of the button that the rocket now pivots about, so
243+
gravity pitches the nose down by a fraction of a degree. With a crosswind, the
244+
aerodynamic moment turns the rocket into the wind before it is fully free ---
245+
the weathercock effect --- and the rocket therefore leaves the rail with a small
246+
angular rate rather than none.
247+
248+
Reading the results
249+
-------------------
250+
251+
The window itself is recorded on the :class:`rocketpy.Flight` object, so a
252+
dispersion study does not have to recover it from the raw solution:
253+
254+
.. list-table::
255+
:header-rows: 1
256+
:widths: 30 70
257+
258+
* - Attribute
259+
- Meaning
260+
* - ``out_of_rail_time``
261+
- Time the *upper* button leaves the rail, starting the window.
262+
* - ``between_rails_time``
263+
- Time the *lower* button leaves the rail, ending the window.
264+
* - ``between_rails_velocity``
265+
- Speed at that moment, the true rail-departure velocity.
266+
* - ``between_rails_state``
267+
- Full state vector handed to the free-flight phase.
268+
* - ``tip_off_duration``
269+
- Length of the window, zero when the phase is disabled.
270+
271+
``Flight.info()`` prints these under a *Tip-Off State* heading whenever the
272+
phase ran, and the attitude plots shade the window so the change in attitude
273+
during tip-off can be read off directly.
274+
275+
References
276+
----------
277+
278+
The tip-off phase and its effect on the initial conditions of free flight are
279+
treated in the launcher dynamics literature: [Chou]_ studies a vehicle moving
280+
along an inclined guideway with dynamic interactions, and [Hosken]_ analyses
281+
tip-off effects in rail launchers.

‎rocketpy/plots/flight_plots.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2513,9 +2513,42 @@ def attitude_data(self, *, filename=None): # pylint: disable=too-many-statement
25132513
ax4.set_title("Euler Spin Angle")
25142514
ax4.grid(True)
25152515

2516+
for ax in (ax1, ax2, ax3, ax4):
2517+
self._mark_tip_off_window(ax)
2518+
25162519
plt.subplots_adjust(hspace=0.5)
25172520
show_or_save_plot(filename)
25182521

2522+
def _mark_tip_off_window(self, ax):
2523+
"""Shades the tip-off window on a time-axis plot.
2524+
2525+
The window runs from the upper rail button leaving the rail to the
2526+
lower one leaving it, which is when the rocket pivots about the lower
2527+
button. It is a no-op unless the flight was run with
2528+
``use_udot_rail2=True``, since otherwise the phase never runs.
2529+
2530+
Parameters
2531+
----------
2532+
ax : matplotlib.axes.Axes
2533+
Axes whose x-axis is flight time, in seconds.
2534+
2535+
Returns
2536+
-------
2537+
None
2538+
"""
2539+
if not self.flight.use_udot_rail2:
2540+
return
2541+
if self.flight.between_rails_time <= self.flight.out_of_rail_time:
2542+
return
2543+
ax.axvspan(
2544+
self.flight.out_of_rail_time,
2545+
self.flight.between_rails_time,
2546+
color="0.5",
2547+
alpha=0.25,
2548+
zorder=0,
2549+
label="Tip-off window",
2550+
)
2551+
25192552
def flight_path_angle_data(self, *, filename=None):
25202553
"""Prints out Flight path and Rocket Attitude angle graphs available
25212554
about the Flight

‎rocketpy/prints/flight_prints.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,32 @@ def out_of_rail_conditions(self):
161161
"Rail Departure Reynolds Number: "
162162
f"{self.flight.reynolds_number(self.flight.out_of_rail_time):.3e}"
163163
)
164+
self.tip_off_conditions()
165+
166+
def tip_off_conditions(self):
167+
"""Prints out the tip-off window, i.e. the interval during which the
168+
rocket pivots about the lower rail button after the upper one has left
169+
the rail. Only printed when the flight was run with
170+
``use_udot_rail2=True``; otherwise the phase never runs and there is
171+
nothing to report.
172+
173+
Returns
174+
-------
175+
None
176+
"""
177+
if not self.flight.use_udot_rail2:
178+
return
179+
print("\nTip-Off State\n")
180+
print(f"Tip-Off Duration: {self.flight.tip_off_duration:.3f} s")
181+
print(f"Lower Button Departure Time: {self.flight.between_rails_time:.3f} s")
182+
print(
183+
"Lower Button Departure Velocity: "
184+
f"{self.flight.between_rails_velocity:.3f} m/s"
185+
)
186+
print(
187+
"Tip-Off Angle of Attack: "
188+
f"{self.flight.angle_of_attack(self.flight.between_rails_time):.3f}°"
189+
)
164190

165191
def burn_out_conditions(self):
166192
"""Prints out the Burn Out Conditions available about the flight,

0 commit comments

Comments
 (0)