ENH: Discrete and Continuous Controllers#946
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #946 +/- ##
===========================================
+ Coverage 80.27% 81.41% +1.14%
===========================================
Files 104 118 +14
Lines 12769 15172 +2403
===========================================
+ Hits 10250 12352 +2102
- Misses 2519 2820 +301 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
538aca0 to
a4de476
Compare
There was a problem hiding this comment.
Pull request overview
Adds first-class support for discrete (fixed refresh rate) and continuous (called every solver step) controllers to RocketPy’s flight simulation, aligning with the controller architecture discussed in issue #274.
Changes:
- Invoke continuous controllers during the solver stepping loop (instead of via fixed time nodes).
- Add
Rocket.add_discrete_controllerandRocket.add_continuous_controllerhelpers that build_Controllerobjects with finite vsinfsampling rates. - Update
_Controllerto defaultsampling_ratetomath.infand document the “infinite sampling” behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
rocketpy/simulation/flight.py |
Calls continuous controllers each solver step and skips time-node creation for sampling_rate=inf. |
rocketpy/rocket/rocket.py |
Adds Rocket-level APIs to register discrete vs continuous controllers. |
rocketpy/control/controller.py |
Defaults controller sampling rate to infinity and updates documentation accordingly. |
Comments suppressed due to low confidence (1)
rocketpy/control/controller.py:81
- The added note in the docstring exceeds the typical line-length used elsewhere in the project (and likely will be reformatted by Ruff/Black). Please wrap this note to keep docstring formatting consistent/readable, and consider clarifying how
sampling_rate=infinteracts with the existing1/sampling_ratewording in thesampling_rateparameter description.
.. note:: The function will be called according to the sampling rate
specified. If unspecified, the default sampling rate is set to infinity, meaning that the
controller function will be called at every step of the simulation.
sampling_rate : float
The sampling rate of the controller function in Hertz (Hz). This
means that the controller function will be called every
`1/sampling_rate` seconds.
MateusStano
left a comment
There was a problem hiding this comment.
Really clean! Just requested a couple of changes and it should be good to merge
Added the requested changes and ready to merge. |
|
Addressed the requested points and pushed follow-up commit 601acb0:\n\n- kept continuous controllers represented by sampling_rate=None\n- ensured continuous controllers do not create time nodes (covered by unit test)\n- updated controller docstring to document sampling_rate as optional/continuous semantics\n- optimized continuous-controller history handling in Flight to avoid rebuilding state history on every step while preserving the documented state-vector format\n\nLocal checks run:\n- pytest tests/unit/simulation/test_flight_time_nodes.py -q\n- pytest tests/unit/simulation/test_flight.py -q\n- |
Gui-FernandesBR
left a comment
There was a problem hiding this comment.
Reviewed the latest commit 601acb0: state-history handling is now efficient and consistent, continuous-controller node scheduling is covered by unit tests, and controller docs reflect optional sampling_rate behavior.
Optimize continuous controller state-history handling to avoid repeated list rebuilding, align controller sampling-rate docs with None-based continuous mode, and add regression coverage to ensure continuous controllers do not create time nodes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
601acb0 to
bb90cdd
Compare
Add unreleased changelog entry for Discrete and Continuous Controllers in PR RocketPy-Team#946. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Gui-FernandesBR |
Resolve conflicts in: - rocketpy/simulation/flight.py: keep develop's logger.debug migration in the verbose block and re-apply the continuous-controller invocation alongside it. - CHANGELOG.md: keep the RocketPy-Team#946 entry plus develop's RocketPy-Team#1043 and RocketPy-Team#924 entries.
…crete The continuous-controller path fed the controller a time-stripped `_controller_state_history` (rows `[x, y, z, vx, vy, vz, ...]`), while the discrete path (__process_sensors_and_controllers_at_current_node) passes `self.solution`, whose rows are time-prefixed (`[t, x, y, z, vx, vy, vz, ...]`). A single controller function therefore received two different state_history layouts depending on discrete vs continuous mode: index [5] was vy for discrete but vz for continuous. Controllers written/tuned against the discrete layout (e.g. the airbrakes example) misbehaved under continuous control -- reading vz where they expected vy, deployment logic exploded and apogee was crushed. Pass `self.solution` directly, matching the discrete path exactly. This makes the two paths behaviorally identical, removes the parallel _controller_state_history list (and its per-step append / initialization), and is O(1) per step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rollers
- Add a single `_Controller.is_continuous` property (sampling_rate is None) and
use it as the one source of truth for the continuous/discrete dispatch in
Flight (__init_controllers and time-node creation), replacing two duplicated
`sampling_rate is None` checks.
- Fix _ControllerPrints.controller_function(): `f"{sampling_rate:.3f}"` raised
TypeError for continuous controllers (sampling_rate=None). Print
"continuous (every solver step)" instead.
- __str__ now reads "with continuous sampling." instead of "... None Hz."
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The state_history handed to controller functions is `Flight.solution`, whose rows are TIME-PREFIXED `[t, x, y, z, vx, vy, vz, ...]` -- one leading time element ahead of the `state` layout, and the last row is the most recent recorded step (not the "previous" state). The docstrings previously described the rows as bare state vectors `[x, y, z, ...]` with `[-1]` as the previous state, an off-by-one-column contract that silently breaks controllers (this is what caused the airbrakes continuous-mode misbehavior). Also document that `sampling_rate` is None for continuous controllers. Updated in _Controller.__init__, _Controller.__call__, and Rocket.add_air_brakes controller docstrings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y layout Adds an integration test that runs a real flight with a continuous air-brakes controller (sampling_rate=None) and asserts it is invoked on every solver step (>50 calls), always receives sampling_rate=None, and receives time-prefixed state_history rows (len == len(state)+1) -- the same layout as the discrete path. Previously only node-skipping was covered; per-step invocation was not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve flight.py (keep develop logger.debug + continuous-controller loop) and re-add the #946 CHANGELOG entry.
- flight.py: collapse the _continuous_controllers comprehension to one line (ruff format --check). - test_flight.py: drop the useless `return None` at the end of the recording controller (pylint R1711). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi @Malmahrouqi3 — thanks for flagging the odd behavior and for attaching Root cause: discrete and continuous handed the controller different
|
* MNT: final fixes before next release Documentation, CHANGELOG and test polish ahead of the next release (v1.13.0), covering PRs merged since v1.12.0. CHANGELOG - Add missing entries: Individual Fins (#818), AIGFS/HRRR forecast models (#951), duplicate-controller fix (#949), Valkyrie flight example (#967). - Hygiene: de-duplicate #958/#966, move #974 to Fixed and #1041 to Removed only, drop the already-released #914 duplicate, and point the logging (#973) and ND-interp (#969) entries at their PRs. - Backfill #940/#941/#944 (shipped in v1.12.0 code but never logged) under the [v1.12.0] section. Docs - New exceptions reference page (rocketpy.exceptions) wired into the reference index; note UnstableRocketWarning in the rocket stability docs (#970). - tanks.rst: switch examples to radius_function= and add a deprecation note (#957). - forecast.rst: fix the HRRR example (missing code directive + stray sentence) (#951). - airbrakes.rst: document discrete vs continuous controllers (sampling_rate=None) (#946). - rocket_usage.rst: note that Parachute is now an abstract base; instantiate HemisphericalParachute (#958). Tests - New regression tests: 3D ND-interp NaN outside convex hull (#969), abstract Parachute cannot be instantiated (#958), Monte Carlo convergence stopping (#922), EnvironmentAnalysis surviving wind API (#1041), radial-burn grain geometry over time (#944), RingClusterMotor full flight (#924), discrete controller invoked once per node (#949), acceleration-based parachute trigger deploys (#911). - Backfill regression tests for ThrustCurve API timeouts (#940) and power_off/on_drag Function objects + _input attributes (#941). * make format

Pull request type
Checklist
black rocketpy/ tests/) has passed locallyCurrent behavior
#274
New behavior
Usage
Tested on the Airbrakes Example (
docs/user/airbrakes.rst)Breaking change
Additional information
There is no node creation for continuous controllers hence this step is skipped in
flight.py. Instead, they are called in each of the ODE solver step, hence not relying on fixed sampling rate.