Skip to content

Commit 90bbf76

Browse files
committed
MNT: address PR #911 review comments
Core changes: - Remove unused altitude_trigger_factory - Restore trigger docstring format with 3/4/5 arg support - Remove backend note about u_dot computation - Remove weathercock_coeff docstring (merge error) - Remove 'legacy support' wording from apogee trigger - Rename 'legacy' test to 'basic' Documentation: - Convert Sensor Noise to note with sensors link - Add numeric altitude trigger example - Remove Performance Considerations and Best Practices - Add logic explanations to all custom trigger examples - Remove duplicate liftoff example - Standardize section naming and format with 'Logic:' and 'Usage:' blocks - Update sensor example for Accelerometer with vector measurements - Reframe example as 'Simulating Drogue Deployment'
1 parent 0e433a6 commit 90bbf76

4 files changed

Lines changed: 123 additions & 96 deletions

File tree

docs/user/parachute_triggers.rst

Lines changed: 116 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,40 @@ Detects apogee when the rocket starts descending.
4242
**Detection criteria:**
4343
- Vertical velocity becomes negative (descent starts)
4444

45+
Numeric Altitude Trigger
46+
------------------------
47+
48+
You can also pass a numeric altitude (int or float) to deploy at a fixed height
49+
while descending.
50+
51+
.. code-block:: python
52+
53+
main = Parachute(
54+
name="Main",
55+
cd_s=10.0,
56+
trigger=400, # meters above ground level
57+
sampling_rate=100,
58+
lag=0.5
59+
)
60+
4561
Custom Triggers
4662
---------------
4763

4864
You can create custom triggers that use acceleration data:
4965

50-
Motor Burnout Trigger (Custom Example)
51-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
66+
.. code-block:: python
67+
68+
from rocketpy import Parachute
69+
70+
Custom Trigger: Motor Burnout
71+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5272

5373
Motor burnout is highly mission-dependent, so it is recommended as a custom
5474
trigger with user-defined thresholds:
5575

76+
Logic: check for a drop in vertical or total acceleration once the rocket is
77+
above a minimum height and still ascending.
78+
5679
.. code-block:: python
5780
5881
def burnout_trigger_factory(
@@ -76,6 +99,10 @@ trigger with user-defined thresholds:
7699
77100
return burnout_trigger
78101
102+
Usage:
103+
104+
.. code-block:: python
105+
79106
drogue = Parachute(
80107
name="Drogue",
81108
cd_s=1.0,
@@ -89,36 +116,65 @@ trigger with user-defined thresholds:
89116
lag=1.5,
90117
)
91118
92-
Apogee by Acceleration (Custom Example)
93-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
119+
Custom Trigger: Apogee by Acceleration
120+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
121+
122+
Logic: near-zero vertical velocity with downward acceleration.
94123

95124
.. code-block:: python
96125
97126
def apogee_acc_trigger(_pressure, _height, state_vector, u_dot):
127+
if u_dot is None or len(u_dot) < 6:
128+
return False
98129
vz = state_vector[5]
99130
az = u_dot[5]
131+
# Apogee is indicated by near-zero vertical velocity and downward acceleration.
100132
return abs(vz) < 1.0 and az < -0.1
101133
102-
Free-fall (Custom Example)
103-
~~~~~~~~~~~~~~~~~~~~~~~~~~
134+
Usage:
135+
136+
.. code-block:: python
137+
138+
main = Parachute(
139+
name="Main",
140+
cd_s=10.0,
141+
trigger=apogee_acc_trigger,
142+
sampling_rate=100,
143+
lag=0.5
144+
)
145+
146+
Custom Trigger: Free-fall
147+
~~~~~~~~~~~~~~~~~~~~~~~~~
148+
149+
Logic: low total acceleration while descending above a small height.
104150

105151
.. code-block:: python
106152
107153
def freefall_trigger(_pressure, height, state_vector, u_dot):
154+
if u_dot is None or len(u_dot) < 6:
155+
return False
108156
ax, ay, az = u_dot[3], u_dot[4], u_dot[5]
109157
total_acc = (ax**2 + ay**2 + az**2) ** 0.5
110158
vz = state_vector[5]
159+
# Free-fall is a low-acceleration phase while descending.
111160
return height > 5.0 and vz < -0.2 and total_acc < 11.5
112161
113-
Liftoff by Acceleration (Custom Example)
114-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
162+
Usage:
115163

116164
.. code-block:: python
117165
118-
def liftoff_trigger(_pressure, _height, _state_vector, u_dot):
119-
ax, ay, az = u_dot[3], u_dot[4], u_dot[5]
120-
total_acc = (ax**2 + ay**2 + az**2) ** 0.5
121-
return total_acc > 15.0
166+
drogue = Parachute(
167+
name="Drogue",
168+
cd_s=1.0,
169+
trigger=freefall_trigger,
170+
sampling_rate=100,
171+
lag=1.5
172+
)
173+
174+
Custom Trigger: Threshold-based
175+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
176+
177+
Logic: deploy when total acceleration drops below a threshold while descending.
122178

123179
.. code-block:: python
124180
@@ -144,6 +200,9 @@ Liftoff by Acceleration (Custom Example)
144200
True to trigger parachute deployment
145201
"""
146202
# Extract acceleration components (m/s²)
203+
if u_dot is None or len(u_dot) < 6:
204+
return False
205+
147206
ax = u_dot[3]
148207
ay = u_dot[4]
149208
az = u_dot[5]
@@ -155,7 +214,10 @@ Liftoff by Acceleration (Custom Example)
155214
vz = state_vector[5]
156215
return total_acc < 5.0 and vz < -10.0
157216
158-
# Use custom trigger
217+
Usage:
218+
219+
.. code-block:: python
220+
159221
parachute = Parachute(
160222
name="Custom",
161223
cd_s=2.0,
@@ -164,10 +226,13 @@ Liftoff by Acceleration (Custom Example)
164226
lag=1.0
165227
)
166228
167-
Sensor and Acceleration Triggers
168-
---------------------------------
229+
Custom Trigger: Sensors and Acceleration
230+
----------------------------------------
231+
232+
Triggers can also accept sensor data alongside acceleration.
169233

170-
Triggers can also accept sensor data alongside acceleration:
234+
Logic: compare measured vertical acceleration from an Accelerometer with the
235+
computed ``u_dot`` value.
171236

172237
.. code-block:: python
173238
@@ -186,18 +251,29 @@ Triggers can also accept sensor data alongside acceleration:
186251
-------
187252
bool
188253
"""
189-
# Access sensor measurements
190-
if len(sensors) > 0:
191-
imu_reading = sensors[0].measurement
254+
# Access accelerometer measurements (vector)
255+
if len(sensors) == 0:
256+
return False
257+
258+
acc_reading = sensors[0].measurement
259+
if acc_reading is None or len(acc_reading) < 3:
260+
return False
192261
193-
# Define threshold for IMU reading (example value)
194-
threshold = 100.0 # Adjust based on sensor units and trigger criteria
262+
if u_dot is None or len(u_dot) < 6:
263+
return False
195264
196-
# Use acceleration data
265+
# Example threshold for measured vertical acceleration (m/s^2)
266+
meas_az = acc_reading[2]
267+
threshold = -5.0
268+
269+
# Use acceleration data from both sensor and dynamics
197270
az = u_dot[5]
198271
199-
# Combine sensor and acceleration logic
200-
return az < -5.0 and imu_reading > threshold
272+
return (az < -5.0) and (meas_az < threshold)
273+
274+
Usage:
275+
276+
.. code-block:: python
201277
202278
parachute = Parachute(
203279
name="Advanced",
@@ -206,46 +282,23 @@ Triggers can also accept sensor data alongside acceleration:
206282
sampling_rate=100
207283
)
208284
209-
Sensor Noise
210-
------------
211-
212-
For realistic IMU behavior, use RocketPy sensors with their own noise models.
213-
Parachute trigger functions can receive ``sensors`` and use those measurements
214-
directly instead of injecting noise in the flight solver.
215-
216-
Performance Considerations
217-
--------------------------
285+
.. note::
218286

219-
Computing acceleration (``u_dot``) requires evaluating the equations of motion,
220-
which adds computational cost. RocketPy optimizes this by:
287+
For realistic IMU behavior, use RocketPy sensors with their own noise models.
288+
Parachute trigger functions can receive ``sensors`` and use those measurements
289+
directly instead of injecting noise in the flight solver. See the
290+
:doc:`Sensor Classes </reference/classes/sensors/index>` for available sensors.
221291

222-
1. **Lazy evaluation**: ``u_dot`` is only computed if the trigger actually needs it
223-
2. **Metadata detection**: The wrapper inspects trigger signatures to determine requirements
224-
3. **Caching**: Derivative evaluations are reused when possible
225-
226-
**Trigger signature detection:**
227-
228-
- 3 parameters ``(p, h, y)``: Legacy trigger, no ``u_dot`` computed
229-
- 4 parameters with ``u_dot``: Only acceleration computed
230-
- 4 parameters with ``sensors``: Only sensors passed
231-
- 5 parameters: Both sensors and acceleration provided
232-
233-
Best Practices
234-
--------------
235-
236-
1. **Choose appropriate sampling rates**: 50-200 Hz is typical for flight computers
237-
2. **Add realistic noise**: Real IMUs have noise; simulate it for validation
238-
3. **Test edge cases**: Verify triggers work at low altitudes, high speeds, etc.
239-
4. **Use robust custom logic**: Add mission-specific guards and thresholds
240-
5. **Document custom triggers**: Include detection criteria in docstrings
241-
242-
Example: Complete Dual-Deploy System
292+
Example: Simulating Drogue Deployment
243293
-------------------------------------
244294

295+
Logic: in RocketPy, only one parachute is active at a time, so you can simulate
296+
dual-deploy by treating the first trigger as a drogue event and the second as the
297+
main deployment.
298+
245299
.. code-block:: python
246300
247301
from rocketpy import Rocket, Parachute, Flight, Environment
248-
import numpy as np
249302
250303
# Environment and rocket setup
251304
env = Environment(latitude=32.99, longitude=-106.97, elevation=1400)
@@ -258,7 +311,7 @@ Example: Complete Dual-Deploy System
258311
if u_dot is None or len(u_dot) < 6:
259312
return False
260313
az = u_dot[5]
261-
vz = state_vector[5] if len(state_vector) > 5 else 0
314+
vz = state_vector[5]
262315
return height > 10 and vz > 1 and az < -8.0
263316
264317
drogue = Parachute(
@@ -271,9 +324,11 @@ Example: Complete Dual-Deploy System
271324
)
272325
my_rocket.add_parachute(drogue)
273326
274-
# Main parachute: Deploy at 800m using custom trigger
275-
def main_deploy_trigger(pressure, height, state_vector, u_dot):
276-
"""Deploy main at 800m while descending with positive vertical acceleration."""
327+
# Main parachute: Deploy at 800m using a custom trigger
328+
def main_deploy_trigger(_pressure, height, state_vector, u_dot):
329+
"""Deploy main at 800m while descending with modest acceleration."""
330+
if u_dot is None or len(u_dot) < 6:
331+
return False
277332
vz = state_vector[5]
278333
az = u_dot[5]
279334
return height < 800 and vz < -5 and az > -15

rocketpy/rocket/parachute.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,6 @@
88
from ..prints.parachute_prints import _ParachutePrints
99

1010

11-
def altitude_trigger_factory(target_altitude, require_descent=True):
12-
"""Return a trigger that deploys when altitude <= target_altitude.
13-
14-
If require_descent is True, also require vertical velocity negative
15-
(descending) to avoid firing during ascent.
16-
"""
17-
18-
def trigger(_pressure, height, state_vector, _u_dot=None):
19-
vz = float(state_vector[5])
20-
if require_descent:
21-
return (height <= target_altitude) and (vz < 0)
22-
return height <= target_altitude
23-
24-
return trigger
25-
26-
2711
class Parachute:
2812
"""Keeps information of the parachute, which is modeled as a hemispheroid.
2913
@@ -69,8 +53,8 @@ class Parachute:
6953
case, the parachute will be ejected when the rocket reaches this height
7054
above ground level while descending.
7155
72-
- A string for built-in triggers:
73-
- ``"apogee"``: Apogee detection (velocity-based)
56+
- The string "apogee" which triggers the parachute at apogee, i.e.,
57+
when the rocket reaches its highest point and starts descending.
7458
7559
7660
Parachute.triggerfunc : function
@@ -84,8 +68,6 @@ class Parachute:
8468
.. note::
8569
8670
The function will be called according to the sampling rate specified.
87-
For performance, ``u_dot`` is only computed if the trigger signature
88-
indicates it is needed.
8971
9072
Parachute.sampling_rate : float
9173
Sampling rate, in Hz, for the trigger function.
@@ -371,7 +353,7 @@ def triggerfunc(p, h, y, sensors, u_dot): # pylint: disable=unused-argument
371353
self.triggerfunc = triggerfunc
372354
return
373355

374-
# Special case: "apogee" (legacy support)
356+
# Special case: "apogee"
375357
if isinstance(trigger, str) and trigger.lower() == "apogee":
376358

377359
def triggerfunc(p, h, y, sensors, u_dot): # pylint: disable=unused-argument

rocketpy/simulation/flight.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -590,16 +590,6 @@ def __init__( # pylint: disable=too-many-arguments,too-many-statements
590590
simulation_mode : str, optional
591591
Simulation mode to use. Can be "6 DOF" for 6 degrees of freedom or
592592
"3 DOF" for 3 degrees of freedom. Default is "6 DOF".
593-
weathercock_coeff : float, optional
594-
Proportionality coefficient (rate coefficient) for the alignment rate of the rocket's body axis
595-
with the relative wind direction in 3-DOF simulations, in rad/s. The actual angular velocity
596-
applied to align the rocket is calculated as ``weathercock_coeff * sin(angle)``, where ``angle``
597-
is the angle between the rocket's axis and the wind direction. A higher value means faster alignment
598-
(quasi-static weathercocking). This parameter is only used when simulation_mode is '3 DOF'.
599-
Default is 0.0 to mimic a pure 3-DOF simulation without any weathercocking (fixed attitude).
600-
Set to a positive value to enable quasi-static weathercocking behaviour.
601-
602-
603593
Returns
604594
-------
605595
None

tests/unit/test_parachute_triggers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,20 @@ def user_trigger(_p, _h, _y, u_dot):
7676
assert np.allclose(recorded["u_dot"][3:6], np.array([-1.0, -2.0, -3.0]))
7777

7878

79-
def test_legacy_trigger_does_not_compute_u_dot():
79+
def test_basic_trigger_does_not_compute_u_dot():
8080
def derivative_func(_t, _y):
8181
raise RuntimeError("derivative should not be called for legacy triggers")
8282

8383
called = {}
8484

85-
def legacy_trigger(_p, _h, _y):
85+
def basic_trigger(_p, _h, _y):
8686
called["ok"] = True
8787
return True
8888

8989
parachute = Parachute(
90-
name="legacy",
90+
name="basic",
9191
cd_s=1.0,
92-
trigger=legacy_trigger,
92+
trigger=basic_trigger,
9393
sampling_rate=100,
9494
)
9595

0 commit comments

Comments
 (0)