|
16 | 16 | PRESETS = { |
17 | 17 | "apogee": lambda **kwargs: ( |
18 | 18 | len(kwargs["flight"].solution) >= 2 |
19 | | - and kwargs["flight"].solution.view(-2)[1]["vz"] > 0 >= kwargs["state"][5] |
| 19 | + and kwargs["flight"].solution.at_index(-2)["vz"] > 0 >= kwargs["state"][5] |
20 | 20 | ), |
21 | 21 | "burnout": lambda **kwargs: ( |
22 | 22 | kwargs.get("time") >= kwargs["rocket"].motor.burn_out_time |
@@ -83,7 +83,26 @@ def __init__( # pylint: disable=too-many-arguments |
83 | 83 | ``step_size`` (float, s), |
84 | 84 | ``height_agl`` (float, m), |
85 | 85 | ``event`` (this :class:`Event` instance), |
86 | | - ``sampling_rate`` (float, Hz or ``None``). |
| 86 | + ``sampling_rate`` (float, Hz or ``None``), |
| 87 | + ``previous_check_state`` (the ``state`` from the previous time this |
| 88 | + event was checked, or ``None`` on the first check), |
| 89 | + ``previous_check_time`` (float, s, or ``None`` on the first check). |
| 90 | +
|
| 91 | + To fire when a value crosses a threshold, compare ``state`` |
| 92 | + against ``previous_check_state``. Those two are always consecutive |
| 93 | + checks of this same event, so a crossing cannot slip between |
| 94 | + them:: |
| 95 | +
|
| 96 | + def trigger(**kwargs): |
| 97 | + previous = kwargs["previous_check_state"] |
| 98 | + if previous is None: |
| 99 | + return False |
| 100 | + return previous[5] > 0 >= kwargs["state"][5] |
| 101 | +
|
| 102 | + Do not use ``state_history[-1]`` for this. That is the last |
| 103 | + *stored* trajectory point, which advances independently of when |
| 104 | + this event is checked, so a crossing can fall between the two and |
| 105 | + never be seen. |
87 | 106 | The following keys are only injected when declared via ``needs``: |
88 | 107 | ``pressure`` (float, Pa), |
89 | 108 | ``state_dot`` (list, time derivative of ``state``), |
@@ -215,6 +234,10 @@ def __init__( # pylint: disable=too-many-arguments |
215 | 234 | self.callback_log = [] |
216 | 235 | self.triggered_times = [] |
217 | 236 | self._trigger_checked = False |
| 237 | + # State and time from the last time this event's trigger was checked. |
| 238 | + # ``None`` until the first check. |
| 239 | + self._previous_state = None |
| 240 | + self._previous_time = None |
218 | 241 |
|
219 | 242 | self.is_discrete = self.sampling_rate is not None |
220 | 243 | self.sampling_interval = ( |
@@ -275,6 +298,8 @@ def reset(self): |
275 | 298 | self.callback_log.clear() |
276 | 299 | self.triggered_times.clear() |
277 | 300 | self._trigger_checked = False |
| 301 | + self._previous_state = None |
| 302 | + self._previous_time = None |
278 | 303 | self.context = deepcopy(self._initial_context) |
279 | 304 |
|
280 | 305 | # Restore enable/disable time history to initial snapshot |
@@ -327,14 +352,27 @@ def __call__(self, trigger_only=False, callback_only=False, reset=True, **kwargs |
327 | 352 |
|
328 | 353 | kwargs["event"] = self |
329 | 354 | kwargs["sampling_rate"] = self.sampling_rate |
| 355 | + # What this event saw the last time it was checked. Comparing against |
| 356 | + # it is the reliable way to notice a value crossing a threshold, since |
| 357 | + # the two states are always consecutive checks of this same event. |
| 358 | + kwargs["previous_check_state"] = self._previous_state |
| 359 | + kwargs["previous_check_time"] = self._previous_time |
330 | 360 |
|
331 | 361 | # --- Trigger Phase --- |
332 | 362 | # Skip evaluating triggers if we are only running the callback. |
333 | 363 | if callback_only is False: |
334 | 364 | if self._call_enable_on(**kwargs) is False: |
335 | 365 | return False |
336 | 366 |
|
337 | | - if self._call_trigger(**kwargs) is False: |
| 367 | + triggered = self._call_trigger(**kwargs) |
| 368 | + |
| 369 | + # Remember this check, whether or not it triggered, so the next one |
| 370 | + # can be compared against it. Recorded before the early return so a |
| 371 | + # trigger that did not fire is still part of the sequence. |
| 372 | + self._previous_state = kwargs.get("state") |
| 373 | + self._previous_time = kwargs.get("time") |
| 374 | + |
| 375 | + if triggered is False: |
338 | 376 | return False |
339 | 377 |
|
340 | 378 | if trigger_only: |
@@ -447,7 +485,7 @@ def _compute_exact_time(self, **kwargs): |
447 | 485 | # The exact-time functions read the state by its canonical position, so |
448 | 486 | # feed them canonical endpoints and a canonical view of the dense |
449 | 487 | # output. The raw dense output is kept so the exact state can be written |
450 | | - # back into the (possibly reduced) tail segment of the solution. |
| 488 | + # back into the (possibly reduced) tail phase of the solution. |
451 | 489 | schema = phase.dynamics.schema |
452 | 490 | raw_interpolator = phase.solver.dense_output() |
453 | 491 | if schema.is_canonical: |
|
0 commit comments