Skip to content

Commit 18aab57

Browse files
committed
ENH: Enable sensors+acceleration triggers in parachutes
Allow parachute triggers to accept (p, h, y, sensors, u_dot); Flight passes sensors+u_dot, computing u_dot on demand with noise; numeric/apogee legacy triggers carry metadata.\n\nTests: pytest tests/unit/test_parachute_trigger_acceleration.py -v\nLint: black rocketpy tests && ruff check rocketpy tests
1 parent 7ec7ac9 commit 18aab57

2 files changed

Lines changed: 62 additions & 39 deletions

File tree

rocketpy/rocket/parachute.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ..mathutils.function import Function
88
from ..prints.parachute_prints import _ParachutePrints
99

10+
1011
def detect_motor_burnout(pressure, height, state_vector, u_dot):
1112
"""Detect motor burnout by sudden drop in acceleration.
1213
@@ -364,13 +365,12 @@ def __evaluate_trigger_function(self, trigger):
364365
365366
Notes
366367
-----
367-
The resulting triggerfunc always has signature (p, h, y, fourth) so
368-
Flight can pass either the sensors list or the u_dot (derivative)
369-
depending on the runtime behaviour.
368+
The resulting triggerfunc always has signature (p, h, y, sensors, u_dot)
369+
so Flight can pass both sensors and u_dot when needed.
370370
"""
371371
# pylint: disable=unused-argument, function-redefined
372372

373-
# Helper to wrap any callable to the internal (p, h, y, fourth) API
373+
# Helper to wrap any callable to the internal (p, h, y, sensors, u_dot) API
374374
def _make_wrapper(fn):
375375
sig = signature(fn)
376376
params = list(sig.parameters.keys())
@@ -380,22 +380,39 @@ def _make_wrapper(fn):
380380
name.lower() in ("u_dot", "udot", "acc", "acceleration")
381381
for name in params[3:]
382382
)
383+
# detect if user function expects sensors argument
384+
expects_sensors = any(name.lower() == "sensors" for name in params[3:])
383385

384-
def wrapper(p, h, y, fourth): # fourth can be sensors or u_dot
385-
# Support both 3- and 4-arg user functions
386+
def wrapper(p, h, y, sensors, u_dot):
387+
# Support 3, 4, and 5-arg user functions
386388
num_params = len(sig.parameters)
387389
if num_params == 3:
388390
return fn(p, h, y)
389391
if num_params == 4:
390-
return fn(p, h, y, fourth)
391-
# fallback: try calling with four args, otherwise three
392+
# Check which 4th arg to pass
393+
fourth_param = params[3].lower()
394+
if fourth_param in ("u_dot", "udot", "acc", "acceleration"):
395+
return fn(p, h, y, u_dot)
396+
else:
397+
return fn(p, h, y, sensors)
398+
if num_params >= 5:
399+
# Pass both sensors and u_dot
400+
return fn(p, h, y, sensors, u_dot)
401+
# fallback: try calling with available args
392402
try:
393-
return fn(p, h, y, fourth)
403+
return fn(p, h, y, sensors, u_dot)
394404
except TypeError:
395-
return fn(p, h, y)
405+
try:
406+
return fn(p, h, y, u_dot)
407+
except TypeError:
408+
try:
409+
return fn(p, h, y, sensors)
410+
except TypeError:
411+
return fn(p, h, y)
396412

397413
# attach metadata so Flight can decide whether to compute u_dot
398414
wrapper._expects_udot = expects_udot
415+
wrapper._expects_sensors = expects_sensors
399416
wrapper._wrapped_fn = fn
400417
return wrapper
401418

@@ -407,12 +424,14 @@ def wrapper(p, h, y, fourth): # fourth can be sensors or u_dot
407424
# Numeric altitude trigger
408425
if isinstance(trigger, (int, float)):
409426

410-
def triggerfunc(p, h, y, sensors): # pylint: disable=unused-argument
427+
def triggerfunc(p, h, y, sensors, u_dot): # pylint: disable=unused-argument
411428
# p = pressure considering parachute noise signal
412429
# h = height above ground level considering parachute noise signal
413430
# y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3]
414431
return y[5] < 0 and h < trigger
415432

433+
triggerfunc._expects_udot = False
434+
triggerfunc._expects_sensors = True
416435
self.triggerfunc = triggerfunc
417436
return
418437

@@ -432,9 +451,11 @@ def triggerfunc(p, h, y, sensors): # pylint: disable=unused-argument
432451
# Special case: "apogee" (legacy support)
433452
if isinstance(trigger, str) and trigger.lower() == "apogee":
434453

435-
def triggerfunc(p, h, y, sensors): # pylint: disable=unused-argument
454+
def triggerfunc(p, h, y, sensors, u_dot): # pylint: disable=unused-argument
436455
return y[5] < 0
437456

457+
triggerfunc._expects_udot = False
458+
triggerfunc._expects_sensors = True
438459
self.triggerfunc = triggerfunc
439460
return
440461

rocketpy/simulation/flight.py

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,13 +1142,11 @@ def __calculate_and_save_pressure_signals(self, parachute, t, z):
11421142
def _evaluate_parachute_trigger(
11431143
self, parachute, pressure, height, y, sensors, derivative_func, t
11441144
):
1145-
"""Evaluate parachute trigger, optionally computing u_dot (acceleration).
1145+
"""Evaluate parachute trigger, passing both sensors and u_dot to wrapper.
11461146
11471147
This helper preserves backward compatibility with existing trigger
1148-
signatures and will compute ``u_dot`` only if the prepared wrapper in
1149-
`Parachute` or the original trigger signature requests an acceleration
1150-
argument (detected by parameter name such as 'u_dot', 'udot', 'acc',
1151-
or 'acceleration').
1148+
signatures. The wrapper in Parachute always expects (p, h, y, sensors, u_dot)
1149+
and Flight computes u_dot only when the trigger requests it (optimization).
11521150
11531151
Parameters
11541152
----------
@@ -1172,44 +1170,48 @@ def _evaluate_parachute_trigger(
11721170
bool
11731171
True if trigger condition met, False otherwise.
11741172
"""
1175-
# Prefer the wrapper metadata: check if the prepared wrapper expects u_dot
11761173
triggerfunc = parachute.triggerfunc
1174+
1175+
# Check wrapper metadata for expectations
11771176
expects_udot = getattr(triggerfunc, "_expects_udot", False)
1177+
expects_sensors = getattr(triggerfunc, "_expects_sensors", True)
11781178

1179-
# If the wrapper didn't advertise, inspect the original user trigger
1180-
if not expects_udot:
1179+
# Fallback: inspect original trigger signature if metadata missing
1180+
if not expects_udot and not expects_sensors:
11811181
trig_original = getattr(parachute, "trigger", None)
11821182
if callable(trig_original):
11831183
try:
11841184
sig = inspect.signature(trig_original)
1185-
params = list(sig.parameters.values())
1185+
params = list(sig.parameters.keys())
11861186
acc_names = {"u_dot", "udot", "acc", "acceleration"}
1187-
if any(p.name.lower() in acc_names for p in params):
1188-
expects_udot = True
1187+
expects_udot = any(p.lower() in acc_names for p in params[3:])
1188+
expects_sensors = any(p.lower() == "sensors" for p in params[3:])
11891189
except (ValueError, TypeError):
11901190
expects_udot = False
1191+
expects_sensors = True
11911192

1192-
# If the trigger expects acceleration, compute u_dot and inject noise
1193+
# Compute u_dot only if needed (performance optimization)
1194+
u_dot = None
11931195
if expects_udot:
11941196
try:
11951197
u_dot = np.array(derivative_func(t, y), dtype=float)
1196-
try:
1197-
noise = np.asarray(self.acceleration_noise_function())
1198-
if noise.size == 3:
1199-
# u_dot layout: [vx, vy, vz, ax, ay, az, ...]
1200-
u_dot[3:6] = u_dot[3:6] + noise
1201-
except Exception:
1202-
# ignore noise errors and continue
1203-
pass
1204-
fourth_arg = u_dot
1198+
# Inject accelerometer noise if configured
1199+
if hasattr(self, "acceleration_noise_function"):
1200+
try:
1201+
noise = np.asarray(self.acceleration_noise_function())
1202+
if noise.size == 3:
1203+
# u_dot layout: [vx, vy, vz, ax, ay, az, ...]
1204+
u_dot[3:6] = u_dot[3:6] + noise
1205+
except Exception:
1206+
# ignore noise errors and continue
1207+
pass
12051208
except Exception:
1206-
# Fallback to sensors if derivative computation fails
1207-
fourth_arg = sensors
1208-
else:
1209-
fourth_arg = sensors
1209+
# If u_dot computation fails, leave as None
1210+
u_dot = None
12101211

1211-
# Call the prepared wrapper (it will forward args to the user's fn)
1212-
return triggerfunc(pressure, height, y, fourth_arg)
1212+
# Call the wrapper with both sensors and u_dot
1213+
# The wrapper will decide which args to pass to the user's function
1214+
return triggerfunc(pressure, height, y, sensors, u_dot)
12131215

12141216
def __init_solution_monitors(self):
12151217
# Initialize solution monitors

0 commit comments

Comments
 (0)