Skip to content

Commit 25651de

Browse files
ENH: Update air brakes controller to support 8-parameter signature and improve environment access
1 parent a8abae4 commit 25651de

9 files changed

Lines changed: 488 additions & 207 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Attention: The newest changes should be on top -->
3232

3333
### Added
3434

35+
- ENH: Air brakes controller functions now support 8-parameter signature [#854](https://github.com/RocketPy-Team/RocketPy/pull/854)
3536
- ENH: Add save functionality to `_MonteCarloPlots.all` method [#848](https://github.com/RocketPy-Team/RocketPy/pull/848)
3637
- ENH: Add persistent caching for ThrustCurve API [#881](https://github.com/RocketPy-Team/RocketPy/pull/881)
3738
- ENH: Compatibility with MERRA-2 atmosphere reanalysis files [#825](https://github.com/RocketPy-Team/RocketPy/pull/825)

docs/user/airbrakes.rst

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,21 @@ Lets define the controller function:
167167
.. jupyter-execute::
168168

169169
def controller_function(
170-
time, sampling_rate, state, state_history, observed_variables, air_brakes
170+
time, sampling_rate, state, state_history, observed_variables, air_brakes, sensors, environment
171171
):
172172
# state = [x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]
173173
altitude_ASL = state[2]
174-
altitude_AGL = altitude_ASL - env.elevation
174+
altitude_AGL = altitude_ASL - environment.elevation
175175
vx, vy, vz = state[3], state[4], state[5]
176176

177177
# Get winds in x and y directions
178-
wind_x, wind_y = env.wind_velocity_x(altitude_ASL), env.wind_velocity_y(altitude_ASL)
178+
wind_x, wind_y = environment.wind_velocity_x(altitude_ASL), environment.wind_velocity_y(altitude_ASL)
179179

180180
# Calculate Mach number
181181
free_stream_speed = (
182182
(wind_x - vx) ** 2 + (wind_y - vy) ** 2 + (vz) ** 2
183183
) ** 0.5
184-
mach_number = free_stream_speed / env.speed_of_sound(altitude_ASL)
184+
mach_number = free_stream_speed / environment.speed_of_sound(altitude_ASL)
185185

186186
# Get previous state from state_history
187187
previous_state = state_history[-1]
@@ -224,6 +224,22 @@ Lets define the controller function:
224224

225225
.. note::
226226

227+
- The ``controller_function`` accepts 6, 7, or 8 parameters for backward
228+
compatibility:
229+
230+
* **6 parameters** (original): ``time``, ``sampling_rate``, ``state``,
231+
``state_history``, ``observed_variables``, ``air_brakes``
232+
* **7 parameters** (with sensors): adds ``sensors`` as the 7th parameter
233+
* **8 parameters** (with environment): adds ``sensors`` and ``environment``
234+
as the 7th and 8th parameters
235+
236+
- The **environment parameter** provides access to atmospheric conditions
237+
(wind, temperature, pressure, elevation) without relying on global variables.
238+
This enables proper serialization of rockets with air brakes and improves
239+
code modularity. Available methods include ``environment.elevation``,
240+
``environment.wind_velocity_x(altitude)``, ``environment.wind_velocity_y(altitude)``,
241+
``environment.speed_of_sound(altitude)``, and others.
242+
227243
- The code inside the ``controller_function`` can be as complex as needed.
228244
Anything can be implemented inside the function, including filters,
229245
apogee prediction, and any controller logic.

rocketpy/control/controller.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ def new_controller_function(
159159
"The arguments must be in the following order: "
160160
"(time, sampling_rate, state_vector, state_history, "
161161
"observed_variables, interactive_objects, sensors, environment). "
162-
"The last two arguments (sensors and environment) are optional."
162+
"Supported signatures: "
163+
"6 parameters (no sensors, no environment), "
164+
"7 parameters (with sensors, no environment), or "
165+
"8 parameters (with sensors and environment)."
163166
)
164167
return new_controller_function
165168

test_environment_parameter.py

Lines changed: 0 additions & 106 deletions
This file was deleted.

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"tests.fixtures.environment.environment_fixtures",
1212
"tests.fixtures.flight.flight_fixtures",
1313
"tests.fixtures.function.function_fixtures",
14+
"tests.fixtures.controller.controller_fixtures",
1415
"tests.fixtures.motor.liquid_fixtures",
1516
"tests.fixtures.motor.hybrid_fixtures",
1617
"tests.fixtures.motor.solid_motor_fixtures",

tests/fixtures/controller/__init__.py

Whitespace-only changes.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import pytest
2+
3+
4+
## Controller
5+
@pytest.fixture
6+
def controller_function():
7+
"""Create a controller function that updates the air brakes deployment level
8+
based on the altitude and vertical velocity of the rocket. This is the same
9+
controller function that is used in the air brakes example in the
10+
documentation.
11+
12+
Returns
13+
-------
14+
function
15+
A controller function
16+
"""
17+
18+
def controller_function( # pylint: disable=unused-argument
19+
time, sampling_rate, state, state_history, observed_variables, air_brakes
20+
):
21+
z = state[2]
22+
vz = state[5]
23+
previous_vz = state_history[-1][5]
24+
if time < 3.9:
25+
return None
26+
if z < 1500:
27+
air_brakes.deployment_level = 0
28+
else:
29+
new_deployment_level = (
30+
air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2
31+
)
32+
if new_deployment_level > air_brakes.deployment_level + 0.2 / sampling_rate:
33+
new_deployment_level = air_brakes.deployment_level + 0.2 / sampling_rate
34+
elif (
35+
new_deployment_level < air_brakes.deployment_level - 0.2 / sampling_rate
36+
):
37+
new_deployment_level = air_brakes.deployment_level - 0.2 / sampling_rate
38+
else:
39+
new_deployment_level = air_brakes.deployment_level
40+
air_brakes.deployment_level = new_deployment_level
41+
42+
return controller_function
43+
44+
45+
@pytest.fixture
46+
def controller_function_with_environment():
47+
"""Create a controller function that uses the environment parameter to access
48+
atmospheric conditions without relying on global variables. This demonstrates
49+
the new environment parameter feature for air brakes controllers.
50+
51+
Returns
52+
-------
53+
function
54+
A controller function that uses environment parameter
55+
"""
56+
57+
def controller_function( # pylint: disable=unused-argument
58+
time,
59+
sampling_rate,
60+
state,
61+
state_history,
62+
observed_variables,
63+
air_brakes,
64+
sensors,
65+
environment,
66+
):
67+
# state = [x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]
68+
altitude_ASL = state[2] # altitude above sea level
69+
altitude_AGL = (
70+
altitude_ASL - environment.elevation
71+
) # altitude above ground level
72+
vx, vy, vz = state[3], state[4], state[5]
73+
74+
# Use environment parameter instead of global variable
75+
wind_x = environment.wind_velocity_x(altitude_ASL)
76+
wind_y = environment.wind_velocity_y(altitude_ASL)
77+
78+
# Calculate Mach number using environment data
79+
free_stream_speed = ((wind_x - vx) ** 2 + (wind_y - vy) ** 2 + (vz) ** 2) ** 0.5
80+
mach_number = free_stream_speed / environment.speed_of_sound(altitude_ASL)
81+
82+
if time < 3.9:
83+
return None
84+
85+
if altitude_AGL < 1500:
86+
air_brakes.deployment_level = 0
87+
else:
88+
previous_vz = state_history[-1][5] if state_history else vz
89+
new_deployment_level = (
90+
air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2
91+
)
92+
# Rate limiting
93+
max_change = 0.2 / sampling_rate
94+
if new_deployment_level > air_brakes.deployment_level + max_change:
95+
new_deployment_level = air_brakes.deployment_level + max_change
96+
elif new_deployment_level < air_brakes.deployment_level - max_change:
97+
new_deployment_level = air_brakes.deployment_level - max_change
98+
99+
air_brakes.deployment_level = new_deployment_level
100+
101+
# Return observed variables including Mach number
102+
return (time, air_brakes.deployment_level, mach_number)
103+
104+
return controller_function

tests/fixtures/function/function_fixtures.py

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -84,102 +84,6 @@ def func_2d_from_csv():
8484
return func
8585

8686

87-
## Controller
88-
@pytest.fixture
89-
def controller_function():
90-
"""Create a controller function that updates the air brakes deployment level
91-
based on the altitude and vertical velocity of the rocket. This is the same
92-
controller function that is used in the air brakes example in the
93-
documentation.
94-
95-
Returns
96-
-------
97-
function
98-
A controller function
99-
"""
100-
101-
def controller_function( # pylint: disable=unused-argument
102-
time, sampling_rate, state, state_history, observed_variables, air_brakes
103-
):
104-
z = state[2]
105-
vz = state[5]
106-
previous_vz = state_history[-1][5]
107-
if time < 3.9:
108-
return None
109-
if z < 1500:
110-
air_brakes.deployment_level = 0
111-
else:
112-
new_deployment_level = (
113-
air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2
114-
)
115-
if new_deployment_level > air_brakes.deployment_level + 0.2 / sampling_rate:
116-
new_deployment_level = air_brakes.deployment_level + 0.2 / sampling_rate
117-
elif (
118-
new_deployment_level < air_brakes.deployment_level - 0.2 / sampling_rate
119-
):
120-
new_deployment_level = air_brakes.deployment_level - 0.2 / sampling_rate
121-
else:
122-
new_deployment_level = air_brakes.deployment_level
123-
air_brakes.deployment_level = new_deployment_level
124-
125-
return controller_function
126-
127-
128-
@pytest.fixture
129-
def controller_function_with_environment():
130-
"""Create a controller function that uses the environment parameter to access
131-
atmospheric conditions without relying on global variables. This demonstrates
132-
the new environment parameter feature for air brakes controllers.
133-
134-
Returns
135-
-------
136-
function
137-
A controller function that uses environment parameter
138-
"""
139-
140-
def controller_function( # pylint: disable=unused-argument
141-
time, sampling_rate, state, state_history, observed_variables, air_brakes, sensors, environment
142-
):
143-
# state = [x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]
144-
altitude_ASL = state[2] # altitude above sea level
145-
altitude_AGL = altitude_ASL - environment.elevation # altitude above ground level
146-
vx, vy, vz = state[3], state[4], state[5]
147-
148-
# Use environment parameter instead of global variable
149-
wind_x = environment.wind_velocity_x(altitude_ASL)
150-
wind_y = environment.wind_velocity_y(altitude_ASL)
151-
152-
# Calculate Mach number using environment data
153-
free_stream_speed = (
154-
(wind_x - vx) ** 2 + (wind_y - vy) ** 2 + (vz) ** 2
155-
) ** 0.5
156-
mach_number = free_stream_speed / environment.speed_of_sound(altitude_ASL)
157-
158-
if time < 3.9:
159-
return None
160-
161-
if altitude_AGL < 1500:
162-
air_brakes.deployment_level = 0
163-
else:
164-
previous_vz = state_history[-1][5] if state_history else vz
165-
new_deployment_level = (
166-
air_brakes.deployment_level + 0.1 * vz + 0.01 * previous_vz**2
167-
)
168-
# Rate limiting
169-
max_change = 0.2 / sampling_rate
170-
if new_deployment_level > air_brakes.deployment_level + max_change:
171-
new_deployment_level = air_brakes.deployment_level + max_change
172-
elif new_deployment_level < air_brakes.deployment_level - max_change:
173-
new_deployment_level = air_brakes.deployment_level - max_change
174-
175-
air_brakes.deployment_level = new_deployment_level
176-
177-
# Return observed variables including Mach number
178-
return (time, air_brakes.deployment_level, mach_number)
179-
180-
return controller_function
181-
182-
18387
@pytest.fixture
18488
def lambda_quad_func():
18589
"""Create a lambda function based on a string.

0 commit comments

Comments
 (0)