Skip to content

fix: accept numpy integer types as Parachute trigger - #1116

Merged
Gui-FernandesBR merged 2 commits into
RocketPy-Team:developfrom
abhi-0203:fix/numpy-integer-trigger
Aug 15, 2026
Merged

fix: accept numpy integer types as Parachute trigger#1116
Gui-FernandesBR merged 2 commits into
RocketPy-Team:developfrom
abhi-0203:fix/numpy-integer-trigger

Conversation

@abhi-0203

Copy link
Copy Markdown

Summary

Replace isinstance(trigger, (int, float)) with isinstance(trigger, Real) and not isinstance(trigger, bool) in Parachute.__init__().

numpy.int64 and numpy.int32 don't subclass Python's int or float, so they were rejected with a ValueError even though they represent valid numeric altitude values. numbers.Real covers all numeric types (int, float, numpy scalars).

Also excludes bool — currently trigger=True is silently accepted as a 1m height, which is never intentional.

Changes

  • rocketpy/rocket/parachute.py: Import Real from numbers, replace isinstance(trigger, (int, float)) with isinstance(trigger, Real) and not isinstance(trigger, bool)

Test plan

  • int 800 → accepted ✓
  • float 800.0 → accepted ✓
  • np.float64(800) → accepted ✓
  • np.int64(800) → accepted ✓
  • np.int32(800) → accepted ✓
  • True / False → rejected ✓ (was previously accepted as height 1m)

AST verified: no old pattern remains, import confirmed.

Closes #1106

@abhi-0203
abhi-0203 requested a review from a team as a code owner August 9, 2026 04:32

@Gui-FernandesBR Gui-FernandesBR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suggest using "NUMERICAL_TYPES" constant from the Function module

@ting-hong-shieh

Copy link
Copy Markdown

I exercised the trigger-type boundary on head e3d2f0fe7c583f3a632d9643dca98b071c60b79f:

Trigger type Result
Python int, NumPy int64 accepted
Python float, NumPy float32 accepted
Python bool, NumPy bool_ rejected with ValueError
Python complex, NumPy complex64 rejected with ValueError

Using numbers.Real with the explicit bool guard gives the intended boundary. A raw NUMERICAL_TYPES check would also include complex values, and Python bool needs its own exclusion, so I would keep the current predicate even though the shared constant was suggested earlier.

Two changes still look necessary before review:

  1. The pull request targets master; the development guide says ordinary fixes should target develop.
  2. The patch has no tests. A parametrized constructor test covering the eight cases above would protect both the NumPy-integer fix and the bool/complex exclusions.

No branch changes were made. Environment: Python 3.12.6; NumPy 2.5.2; RocketPy PR head above; macOS 26.5.2 arm64.

@Gui-FernandesBR
Gui-FernandesBR changed the base branch from master to develop August 14, 2026 09:45
@Gui-FernandesBR

Copy link
Copy Markdown
Member

I understand this PR still needs some fixes.

  1. Adding unit tests
  2. Using the NUMERICAL_TYPES from the Function module

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.45%. Comparing base (be195d4) to head (4a29ccb).
⚠️ Report is 8 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1116      +/-   ##
===========================================
+ Coverage    84.33%   84.45%   +0.11%     
===========================================
  Files          130      131       +1     
  Lines        17266    17495     +229     
===========================================
+ Hits         14562    14775     +213     
- Misses        2704     2720      +16     

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

isinstance(trigger, (int, float)) rejects numpy integer types
(np.int64, np.int32) because they don't subclass Python's int or float.
Replace with isinstance(trigger, numbers.Real) which covers all numeric
types (int, float, numpy scalars) while excluding bool.

Fixes RocketPy-Team#1106
@Gui-FernandesBR
Gui-FernandesBR force-pushed the fix/numpy-integer-trigger branch from e3d2f0f to 7bb58b7 Compare August 15, 2026 12:24
@Gui-FernandesBR

Copy link
Copy Markdown
Member

Thanks for the boundary table — that settles the NUMERICAL_TYPES question, and you are right to keep your predicate. I checked the constant:

NUMERICAL_TYPES = (float, int, complex, np.integer, np.floating, np.complexfloating)

so it would accept complex / np.complex64, and because bool subclasses int it would not reject True either. numbers.Real plus the explicit bool guard is the correct boundary here. Dropping that request — please ignore item 2 of my earlier comment.

What is still missing is item 1, the tests. The diff is still only the one-line predicate change in parachute.py. Please add unit tests (tests/unit/rocket/test_parachute.py or tests/unit/test_parachute_triggers.py) that pin exactly the table you posted:

  • int and np.int64 accepted as an altitude trigger
  • float and np.float32 accepted
  • bool and np.bool_ rejected with ValueError
  • complex and np.complex64 rejected with ValueError

That way the boundary you measured by hand is the boundary CI enforces from now on. With those in, this is good to go.

One note: the workflow runs on this PR had been sitting in action_required and never actually executed, so the green/absent checks were not meaningful. I approved them, so you should get real results now.

@Gui-FernandesBR

Copy link
Copy Markdown
Member

Correcting my previous comment: I said "with tests, this is good to go" before CI had run. Now that the runs are approved and have actually executed, this is red on all six Pytest legs, and the reason is more interesting than a missing test.

FAILED tests/unit/stochastic/test_stochastic_parachute.py::test_a_numpy_integer_is_refused_here_because_parachute_refuses_it[800_0]
FAILED tests/unit/stochastic/test_stochastic_parachute.py::test_a_numpy_integer_is_refused_here_because_parachute_refuses_it[800_1]
Failed: DID NOT RAISE ValueError

That test came in with #1111 and it deliberately pins the asymmetry you are removing. Its docstring even points at this PR as the right fix:

Parachute checks isinstance(trigger, (int, float)). numpy.float64 subclasses float and passes; numpy.int64 subclasses neither and raises. So this check matches that one rather than numbers.Real [...] The asymmetry is Parachute's and is worth fixing there.

So you are doing exactly what that note asked for — but the change is only half applied, and that is what makes the suite red:

1. rocketpy/stochastic/stochastic_parachute.py, _is_a_trigger (line 8) still carries the narrow check, and its docstring documents the now-inverted rationale:

return isinstance(member, (int, float)) and not isinstance(member, bool)

Because StochasticParachute._validate_trigger uses it, a Monte Carlo user still cannot pass np.int64(800) as a trigger even after your fix. Please widen it to the same predicate you used in Parachute and rewrite that docstring — the "widening here only moves the failure to create time" reasoning stops being true the moment Parachute accepts it.

2. test_a_numpy_integer_is_refused_here_because_parachute_refuses_it now asserts the opposite of the intended behaviour and has to be rewritten — name included, since the name states the old contract. It should become the positive case: a numpy integer is accepted by both Parachute and StochasticParachute.

3. Plus the tests I asked for in my previous comment, covering the boundary table you measured.

Sorry for the premature green light — the checks were sitting unapproved, so there was nothing to read.

This PR widened Parachute's height check from `(int, float)` to
`numbers.Real`, so a height read out of a NumPy array is accepted.
StochasticParachute validates the same triggers before a Parachute is ever
built, and its copy of the check was still spelled `(int, float)`. A
`numpy.int64` height was therefore still refused there, even though the
Parachute it would have built accepts it.

That mismatch is what turned all six Pytest legs red:
`test_a_numpy_integer_is_refused_here_because_parachute_refuses_it` pinned
the old asymmetry, and its own docstring said the fix belonged in Parachute.

Rather than restate the predicate a second time, Parachute now exposes it as
`_is_a_height_trigger` and stochastic/ calls that. The two spellings drifted
apart once already; sharing one definition is what stops it happening again.

Tests:

- the NumPy integers move into
  `test_what_this_accepts_is_what_a_parachute_accepts`, where they now
  belong, joined by `numpy.float32`
- the refusal test is rewritten around what is still refused by both:
  `numpy.bool_` and the complex types, none of which are `Real`
- a new test asserts the agreement itself over the whole boundary, so
  changing one side alone fails with both verdicts printed
- Parachute gains boundary tests of its own, including that `True` is still
  refused rather than read as a height of one metre

Verified: reverting either side alone turns these red (the drift guard
reports `np.float32(800.0): stochastic/ says False, Parachute says True`);
ruff check and format clean; pylint 10.00/10; tests/unit 2098 passed,
16 skipped.

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

Copy link
Copy Markdown
Member

I pushed the rest of this to your branch (4a29ccb4) rather than send you round again — CI is now green on all 9 checks.

The missing half was that StochasticParachute validates the same triggers before a Parachute is ever built, and its copy of the height check was still (int, float). So a numpy.int64 height was still refused there even after your fix, and that is what kept the six Pytest legs red.

Rather than write the predicate out a second time, Parachute now exposes it as _is_a_height_trigger and stochastic/ calls it. The two spellings had already drifted apart once — sharing one definition is what keeps them from drifting again, which is the actual root cause here.

On tests, I did four things:

  • Moved np.int64 / np.int32 into test_what_this_accepts_is_what_a_parachute_accepts, where they now belong, and added np.float32 — which was refused for the same reason and nobody had noticed.
  • Rewrote test_a_numpy_integer_is_refused_here_because_parachute_refuses_it (from BUG: validation in stochastic/ that survives python -O #1111) around what is still refused by both sides. It asserted the opposite of the new behaviour, name included.
  • Added test_neither_check_can_drift_from_the_other_again, which asserts the agreement between the two checks over the whole boundary instead of a list of types on either side. Revert either side alone and it fails with both verdicts printed: np.float32(800.0): stochastic/ says False, Parachute says True.
  • Gave Parachute boundary tests of its own in tests/unit/rocket/test_parachute.py, covering the table you measured by hand — including that True is still refused rather than read as a height of one metre.

Your numbers.Real + bool guard turns out to be exactly the right boundary, and it needs no special case for numpy.bool_: that is not a numbers.Real at all (its MRO is bool → generic → object), so it falls through to the error on its own. Same for the complex types. I verified all eleven cases from your table individually.

Verified before pushing: ruff check and ruff format clean, pylint 10.00/10 on all four files, tests/unit 2098 passed / 16 skipped locally, and reverting either side of the predicate turns the new tests red.

Thanks for the boundary table in your earlier comment — it was the thing that made the right predicate obvious, and it's now encoded in the suite.

@Gui-FernandesBR
Gui-FernandesBR merged commit df06506 into RocketPy-Team:develop Aug 15, 2026
9 checks passed
Gui-FernandesBR added a commit to thatrandomasiandev/RocketPy that referenced this pull request Aug 15, 2026
RocketPy-Team#1116 widened the height check to numbers.Real and, to stop the two copies
of that check drifting apart, gave Parachute a single _is_a_height_trigger
that stochastic/ now calls. This branch had edited the same predicate and
the same two test modules, so it conflicted in three files. The two changes
compose better than either alone:

- _is_a_trigger keeps its ("time", t_deploy) branch, and the height case
  defers to _is_a_height_trigger as RocketPy-Team#1116 intended.

- The time delay now uses that same predicate instead of a float() coercion
  wrapped in try/except. That was a real disagreement waiting to happen:
  float("3.0") succeeds, so ("time", "3.0") was accepted by Parachute while
  the stochastic check refused it. A string delay is now refused on both
  sides rather than quietly coerced.

- RocketPy-Team#1116's drift guard grew the time forms, so it now covers this shape too.
  Reverting the Parachute side to the lenient coercion makes it fail with
  ('time', '3.0'): stochastic/ says False, Parachute says True.

- Both test modules had appended to the same lists and to the end of the
  same files; kept both sides throughout, and added ("time", "3.0") to the
  refused-trigger cases.

Verified: tests/unit 2154 passed, 17 skipped; the parachute unit modules and
the new integration test give 89 passed; ruff check and format clean; pylint
10.00/10.

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

Copy link
Copy Markdown

Thanks for digging into this and carrying the fix through! Glad the boundary testing was useful.

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.

Parachute refuses a numpy integer trigger but accepts a numpy float

3 participants